aboutsummaryrefslogtreecommitdiff
path: root/plugins/VNLib.Plugins.Essentials.SocialOauth/src/SocialOauthBase.cs
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2023-03-09 01:48:39 -0500
committerLibravatar vnugent <public@vaughnnugent.com>2023-03-09 01:48:39 -0500
commit03f3226ea055dca3565bb859437624ef04a236fd (patch)
treec3aae503ae9b459a6fcaf9a18891d11ee8e1d1d8 /plugins/VNLib.Plugins.Essentials.SocialOauth/src/SocialOauthBase.cs
parent0e78874a09767aa53122a7242a8da7021020c1a2 (diff)
Omega cache, session, and account provider complete overhaul
Diffstat (limited to 'plugins/VNLib.Plugins.Essentials.SocialOauth/src/SocialOauthBase.cs')
-rw-r--r--plugins/VNLib.Plugins.Essentials.SocialOauth/src/SocialOauthBase.cs499
1 files changed, 331 insertions, 168 deletions
diff --git a/plugins/VNLib.Plugins.Essentials.SocialOauth/src/SocialOauthBase.cs b/plugins/VNLib.Plugins.Essentials.SocialOauth/src/SocialOauthBase.cs
index 73c2ab5..2ad3a8e 100644
--- a/plugins/VNLib.Plugins.Essentials.SocialOauth/src/SocialOauthBase.cs
+++ b/plugins/VNLib.Plugins.Essentials.SocialOauth/src/SocialOauthBase.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2022 Vaughn Nugent
+* Copyright (c) 2023 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Essentials.SocialOauth
@@ -25,6 +25,7 @@
using System;
using System.Net;
using System.Text;
+using System.Buffers;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
@@ -32,6 +33,7 @@ using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text.Json.Serialization;
using System.Runtime.InteropServices;
+using System.Diagnostics.CodeAnalysis;
using FluentValidation;
@@ -39,17 +41,18 @@ using RestSharp;
using VNLib.Net.Http;
using VNLib.Net.Rest.Client;
using VNLib.Hashing;
+using VNLib.Hashing.IdentityUtility;
using VNLib.Utils;
using VNLib.Utils.Memory;
using VNLib.Utils.Logging;
using VNLib.Utils.Extensions;
-using VNLib.Utils.Memory.Caching;
using VNLib.Plugins.Essentials.Users;
using VNLib.Plugins.Essentials.Accounts;
using VNLib.Plugins.Essentials.Endpoints;
using VNLib.Plugins.Essentials.Extensions;
-using VNLib.Plugins.Extensions.Validation;
using VNLib.Plugins.Essentials.SocialOauth.Validators;
+using VNLib.Plugins.Extensions.Loading;
+using VNLib.Plugins.Extensions.Validation;
namespace VNLib.Plugins.Essentials.SocialOauth
{
@@ -62,11 +65,17 @@ namespace VNLib.Plugins.Essentials.SocialOauth
const string AUTH_ERROR_MESSAGE = "You have no pending authentication requests.";
const string AUTH_GRANT_SESSION_NAME = "auth";
+ const string SESSION_SIG_KEY_NAME = "soa.sig";
+ const string SESSION_TOKEN_KEY_NAME = "soa.tkn";
+ const string CLAIM_COOKIE_NAME = "extern-claim";
+ const int SIGNING_KEY_SIZE = 32;
+
+ private static HMAC GetSigningAlg(byte[] key) => new HMACSHA256(key);
/// <summary>
/// The client configuration struct passed during base class construction
/// </summary>
- protected abstract OauthClientConfig Config { get; }
+ protected virtual OauthClientConfig Config { get; }
///<inheritdoc/>
protected override ProtectionSettings EndpointProtectionSettings { get; } = new()
@@ -82,17 +91,13 @@ namespace VNLib.Plugins.Essentials.SocialOauth
/// The resst client connection pool
/// </summary>
protected RestClientPool ClientPool { get; }
-
- private readonly Dictionary<string, LoginClaim> ClaimStore;
- private readonly Dictionary<string, OAuthAccessState> AuthorizationStore;
+
private readonly IValidator<LoginClaim> ClaimValidator;
private readonly IValidator<string> NonceValidator;
private readonly IValidator<AccountData> AccountDataValidator;
- protected SocialOauthBase()
+ protected SocialOauthBase(PluginBase plugin, IConfigScope config)
{
- ClaimStore = new(StringComparer.OrdinalIgnoreCase);
- AuthorizationStore = new(StringComparer.OrdinalIgnoreCase);
ClaimValidator = GetClaimValidator();
NonceValidator = GetNonceValidator();
AccountDataValidator = new AccountDataValidator();
@@ -108,6 +113,12 @@ namespace VNLib.Plugins.Essentials.SocialOauth
//Configure rest client to comunications to main discord api
ClientPool = new(10, poolOptions, StaticClientPoolInitializer);
+
+ //Get the configuration element for the derrived type
+ Config = plugin.CreateService<OauthClientConfig>(config);
+
+ //Init endpoint
+ InitPathAndLog(Config.EndpointPath, plugin.Log);
}
private static IValidator<LoginClaim> GetClaimValidator()
@@ -133,6 +144,7 @@ namespace VNLib.Plugins.Essentials.SocialOauth
return val;
}
+ ///<inheritdoc/>
protected override ERRNO PreProccess(HttpEntity entity)
{
if (!base.PreProccess(entity))
@@ -150,7 +162,7 @@ namespace VNLib.Plugins.Essentials.SocialOauth
}
//Make sure the user is not logged in
- return !(entity.LoginCookieMatches() || entity.TokenMatches());
+ return !entity.IsClientAuthorized(AuthorzationCheckLevel.Any);
}
/// <summary>
@@ -176,8 +188,7 @@ namespace VNLib.Plugins.Essentials.SocialOauth
/// A task the resolves the <see cref="OAuthAccessState"/> that includes all relavent
/// authorization data. Result may be null if authorzation is invalid or not granted
/// </returns>
- /// <param name="cancellationToken"></param>
- protected async Task<OAuthAccessState?> ExchangeCodeForTokenAsync(HttpEntity ev, string code, CancellationToken cancellationToken)
+ protected virtual async Task<OAuthAccessState?> ExchangeCodeForTokenAsync(HttpEntity ev, string code, CancellationToken cancellationToken)
{
//valid response, time to get the actual authorization from gh for client
RestRequest request = new(Config.AccessTokenUrl, Method.Post);
@@ -207,7 +218,6 @@ namespace VNLib.Plugins.Essentials.SocialOauth
/// <param name="clientAccess">The access state from the code/token exchange</param>
/// <param name="cancellationToken">A token to cancel the operation</param>
/// <returns>The user's account data, null if not account exsits on the remote site, and process cannot continue</returns>
- /// <param name="cancellationToken"></param>
protected abstract Task<AccountData?> GetAccountDataAsync(IOAuthAccessState clientAccess, CancellationToken cancellationToken);
/// <summary>
/// Gets an object that represents the required information for logging-in a user (namley unique user-id)
@@ -217,45 +227,94 @@ namespace VNLib.Plugins.Essentials.SocialOauth
/// <returns></returns>
protected abstract Task<UserLoginData?> GetLoginDataAsync(IOAuthAccessState clientAccess, CancellationToken cancellation);
- sealed class LoginClaim : ICacheable, INonce
+ sealed class LoginClaim : IClientSecInfo
{
[JsonPropertyName("public_key")]
public string? PublicKey { get; set; }
+
[JsonPropertyName("browser_id")]
public string? ClientId { get; set; }
- /// <summary>
- /// The raw OAuth flow state parameter the client must decrypt before
- /// navigating to remote authentication source
- /// </summary>
- [JsonIgnore]
- public ReadOnlyMemory<byte> RawNonce { get; private set; }
- [JsonIgnore]
- DateTime ICacheable.Expires { get; set; }
- bool IEquatable<ICacheable>.Equals(ICacheable? other) => Equals(other);
- void ICacheable.Evicted()
- {
- //Erase nonce
- MemoryUtil.UnsafeZeroMemory(RawNonce);
- }
+ [JsonPropertyName("exp")]
+ public long ExpirationSeconds { get; set; }
- public override bool Equals(object? obj)
+ [JsonPropertyName("iat")]
+ public long IssuedAtTime { get; set; }
+
+ [JsonPropertyName("nonce")]
+ public string? Nonce { get; set; }
+
+ public void ComputeNonce(int nonceSize)
{
- return obj is LoginClaim otherClaim && this.PublicKey!.Equals(otherClaim.PublicKey, StringComparison.Ordinal);
+ byte[] buffer = ArrayPool<byte>.Shared.Rent(nonceSize);
+ try
+ {
+ Span<byte> nonce = buffer.AsSpan(0, nonceSize);
+
+ //get random data
+ RandomHash.GetRandomBytes(nonce);
+
+ //Encode nonce
+ Nonce = VnEncoding.ToBase32String(nonce);
+ }
+ finally
+ {
+ MemoryUtil.InitializeBlock(buffer.AsSpan());
+ ArrayPool<byte>.Shared.Return(buffer);
+ }
}
- public override int GetHashCode() => PublicKey!.GetHashCode();
+ }
+
+ /*
+ * Claims are considered indempodent because they require no previous state
+ * and will return a new secret authentication "token" (url + nonce) that
+ * uniquely identifies the claim and authorization upgrade later
+ */
+
+ protected override async ValueTask<VfReturnType> PutAsync(HttpEntity entity)
+ {
+ ValErrWebMessage webm = new();
- void INonce.ComputeNonce(Span<byte> buffer)
+ //Get the login message
+ LoginClaim? claim = await entity.GetJsonFromFileAsync<LoginClaim>();
+
+ if (webm.Assert(claim != null, "Emtpy message body"))
{
- RandomHash.GetRandomBytes(buffer);
- //Store copy
- RawNonce = buffer.ToArray();
+ entity.CloseResponseJson(HttpStatusCode.BadRequest, webm);
+ return VfReturnType.VirtualSkip;
}
- bool INonce.VerifyNonce(ReadOnlySpan<byte> nonceBytes)
+ //Validate the message
+ if (!ClaimValidator.Validate(claim, webm))
{
- return CryptographicOperations.FixedTimeEquals(RawNonce.Span, nonceBytes);
+ entity.CloseResponseJson(HttpStatusCode.UnprocessableEntity, webm);
+ return VfReturnType.VirtualSkip;
}
+
+ //Configure the login claim
+ claim.IssuedAtTime = entity.RequestedTimeUtc.ToUnixTimeSeconds();
+
+ //Set expiration time in seconds
+ claim.ExpirationSeconds = entity.RequestedTimeUtc.Add(Config.InitClaimValidFor).ToUnixTimeMilliseconds();
+
+ //Set nonce
+ claim.ComputeNonce((int)Config.NonceByteSize);
+
+ //Build the redirect uri
+ webm.Result = new LoginUriBuilder()
+ .WithEncoding(entity.Server.Encoding)
+ .WithUrl(entity.IsSecure ? "https" : "http", entity.Server.RequestUri.Authority, Path)
+ .WithNonce(claim.Nonce!)
+ .Build(Config)
+ .Encrypt(entity, claim);
+
+ //Sign and set the claim cookie
+ SignAndSetCookie(entity, claim);
+
+ webm.Success = true;
+ //Response
+ entity.CloseResponse(webm);
+ return VfReturnType.VirtualSkip;
}
/*
@@ -272,65 +331,61 @@ namespace VNLib.Plugins.Essentials.SocialOauth
{
//Disable refer headers when nonce is set
entity.Server.Headers["Referrer-Policy"] = "no-referrer";
-
+
//Check for security navigation headers. This should be a browser redirect,
if (!entity.Server.IsNavigation() || !entity.Server.IsUserInvoked())
{
+ ClearClaimData(entity);
//The connection was not a browser redirect
entity.Redirect(RedirectType.Temporary, $"{Path}?result=bad_sec");
return VfReturnType.VirtualSkip;
}
-
+
//Try to get the claim from the state parameter
- if (ClaimStore.TryGetOrEvictRecord(state, out LoginClaim? claim) < 1)
+ if (!VerifyAndGetClaim(entity, out LoginClaim? claim))
{
+ ClearClaimData(entity);
entity.Redirect(RedirectType.Temporary, $"{Path}?result=expired");
return VfReturnType.VirtualSkip;
}
-
- //Lock on the claim to prevent replay
- lock (claim)
- {
- bool isValid = claim.VerifyNonce(state);
- //Evict the record inside the lock, also wipes nonce contents
- ClaimStore.EvictRecord(state);
- //Compare binary values of nonce incase of dicionary collision
- if (!isValid)
- {
- entity.Redirect(RedirectType.Temporary, $"{Path}?result=invalid");
- return VfReturnType.VirtualSkip;
- }
+ //Confirm the nonce matches the claim
+ if (string.CompareOrdinal(claim.Nonce, state) != 0)
+ {
+ ClearClaimData(entity);
+ entity.Redirect(RedirectType.Temporary, $"{Path}?result=invalid");
+ return VfReturnType.VirtualSkip;
}
-
+
//Exchange the OAuth code for a token (application specific)
OAuthAccessState? token = await ExchangeCodeForTokenAsync(entity, code, entity.EventCancellation);
-
+
//Token may be null
- if(token == null)
+ if (token == null)
{
+ ClearClaimData(entity);
entity.Redirect(RedirectType.Temporary, $"{Path}?result=invalid");
return VfReturnType.VirtualSkip;
}
-
- //Store claim info
- token.PublicKey = claim.PublicKey;
- token.ClientId = claim.ClientId;
-
- //Generate the new nonce
- string nonce = token.ComputeNonce((int)Config.NonceByteSize);
- //Collect expired records
- AuthorizationStore.CollectRecords();
- //Register the access token
- AuthorizationStore.StoreRecord(nonce, token, Config.LoginNonceLifetime);
+
+ //Create the new nonce
+ claim.ComputeNonce((int)Config.NonceByteSize);
+
+ //Store access state in the user's session
+ entity.Session.SetObject(SESSION_TOKEN_KEY_NAME, token);
+
+ //Sign and set cookie
+ SignAndSetCookie(entity, claim);
+
//Prepare redirect
- entity.Redirect(RedirectType.Temporary, $"{Path}?result=authorized&nonce={nonce}");
+ entity.Redirect(RedirectType.Temporary, $"{Path}?result=authorized&nonce={claim.Nonce}");
return VfReturnType.VirtualSkip;
}
//Check to see if there was an error code set
if (entity.QueryArgs.TryGetNonEmptyValue("error", out string? errorCode))
{
+ ClearClaimData(entity);
Log.Debug("{Type} error {err}:{des}", Config.AccountOrigin, errorCode, entity.QueryArgs["error_description"]);
entity.Redirect(RedirectType.Temporary, $"{Path}?result=error");
return VfReturnType.VirtualSkip;
@@ -358,6 +413,7 @@ namespace VNLib.Plugins.Essentials.SocialOauth
//Recover the nonce
string? base32Nonce = request.RootElement.GetPropString("nonce");
+
if(webm.Assert(base32Nonce != null, message: "Nonce parameter is required"))
{
entity.CloseResponseJson(HttpStatusCode.UnprocessableEntity, webm);
@@ -370,29 +426,30 @@ namespace VNLib.Plugins.Essentials.SocialOauth
entity.CloseResponseJson(HttpStatusCode.UnprocessableEntity, webm);
return VfReturnType.VirtualSkip;
}
-
+
//Recover the access token
- if (AuthorizationStore.TryGetOrEvictRecord(base32Nonce!, out OAuthAccessState? token) < 1)
+ bool cookieValid = VerifyAndGetClaim(entity, out LoginClaim? claim);
+
+ if (webm.Assert(cookieValid, AUTH_ERROR_MESSAGE))
{
- webm.Result = AUTH_ERROR_MESSAGE;
entity.CloseResponse(webm);
return VfReturnType.VirtualSkip;
}
-
- bool valid;
- //Valid token, now verify the nonce within the locked context
- lock (token)
- {
- valid = token.VerifyNonce(base32Nonce);
- //Evict (wipes nonce)
- AuthorizationStore.EvictRecord(base32Nonce!);
- }
-
- if (webm.Assert(valid, AUTH_ERROR_MESSAGE))
+
+ //We can clear the client's access claim
+ ClearClaimData(entity);
+
+ //Confirm nonce matches the client's nonce string
+ bool nonceValid = string.CompareOrdinal(claim.Nonce, base32Nonce) == 0;
+
+ if (webm.Assert(nonceValid, AUTH_ERROR_MESSAGE))
{
entity.CloseResponse(webm);
return VfReturnType.VirtualSkip;
- }
+ }
+
+ //Safe to recover the access token
+ IOAuthAccessState token = entity.Session.GetObject<OAuthAccessState>(SESSION_TOKEN_KEY_NAME);
//get the user's login information (ie userid)
UserLoginData? userLogin = await GetLoginDataAsync(token, entity.EventCancellation);
@@ -430,12 +487,9 @@ namespace VNLib.Plugins.Essentials.SocialOauth
entity.CloseResponse(webm);
return VfReturnType.VirtualSkip;
}
- //Create new user, create random passwords
- byte[] randomPass = RandomHash.GetRandomBytes(Config.RandomPasswordSize);
+
//Generate a new random passowrd incase the user wants to use a local account to log in sometime in the future
- PrivateString passhash = Config.Passwords.Hash(randomPass);
- //overwite the password bytes
- MemoryUtil.InitializeBlock(randomPass.AsSpan());
+ using PrivateString passhash = Config.Passwords.GetRandomPassword(Config.RandomPasswordSize);
try
{
//Create the user with the specified email address, minimum privilage level, and an empty password
@@ -454,10 +508,6 @@ namespace VNLib.Plugins.Essentials.SocialOauth
entity.CloseResponse(webm);
return VfReturnType.VirtualSkip;
}
- finally
- {
- passhash.Dispose();
- }
}
else
{
@@ -467,12 +517,14 @@ namespace VNLib.Plugins.Essentials.SocialOauth
entity.CloseResponse(webm);
return VfReturnType.VirtualSkip;
}
+
//Make sure local accounts are allowed
if (webm.Assert(!user.IsLocalAccount() || Config.AllowForLocalAccounts, AUTH_ERROR_MESSAGE))
{
entity.CloseResponse(webm);
return VfReturnType.VirtualSkip;
}
+
//Reactivate inactive accounts
if(user.Status == UserStatus.Inactive)
{
@@ -490,14 +542,17 @@ namespace VNLib.Plugins.Essentials.SocialOauth
try
{
//Generate authoization
- webm.Token = entity.GenerateAuthorization(token.PublicKey!, token.ClientId!, user);
+ entity.GenerateAuthorization(claim, user, webm);
+
//Store the user current oauth information in the current session for others to digest
entity.Session.SetObject($"{Config.AccountOrigin}.{AUTH_GRANT_SESSION_NAME}", token);
+
//Send the username back to the client
webm.Result = new AccountData()
{
EmailAddress = user.EmailAddress,
};
+
//Set the success flag
webm.Success = true;
//Write to log
@@ -520,7 +575,10 @@ namespace VNLib.Plugins.Essentials.SocialOauth
webm.Token = null;
webm.Result = AUTH_ERROR_MESSAGE;
webm.Success = false;
+
+ //destroy any login data on failure
entity.InvalidateLogin();
+
Log.Error(uue);
}
finally
@@ -530,52 +588,10 @@ namespace VNLib.Plugins.Essentials.SocialOauth
entity.CloseResponse(webm);
return VfReturnType.VirtualSkip;
}
+
+
/*
- * Claims are considered indempodent because they require no previous state
- * and will return a new secret authentication "token" (url + nonce) that
- * uniquely identifies the claim and authorization upgrade later
- */
-
- protected override async ValueTask<VfReturnType> PutAsync(HttpEntity entity)
- {
- ValErrWebMessage webm = new();
-
- //Get the login message
- LoginClaim? claim = await entity.GetJsonFromFileAsync<LoginClaim>();
-
- if (webm.Assert(claim != null, "Emtpy message body"))
- {
- entity.CloseResponseJson(HttpStatusCode.BadRequest, webm);
- return VfReturnType.VirtualSkip;
- }
-
- //Validate the message
- if (!ClaimValidator.Validate(claim, webm))
- {
- entity.CloseResponseJson(HttpStatusCode.UnprocessableEntity, webm);
- return VfReturnType.VirtualSkip;
- }
-
- //Cleanup old records
- ClaimStore.CollectRecords();
-
- //Set nonce
- string base32Nonce = claim.ComputeNonce((int)Config.NonceByteSize);
-
- //build the redirect url
- webm.Result = BuildUrl(base32Nonce, claim.PublicKey!, entity.IsSecure ? "https" : "http", entity.Server.RequestUri.Authority, entity.Server.Encoding);
-
- //Store the claim
- ClaimStore.StoreRecord(base32Nonce, claim, Config.LoginNonceLifetime);
-
- webm.Success = true;
- //Response
- entity.CloseResponse(webm);
- return VfReturnType.VirtualSkip;
- }
-
- /*
* Construct the client's redirect url based on their login claim, which contains
* a public key which can be used to encrypt the url so that only the client
* private-key holder can decrypt the url and redirect themselves to the
@@ -584,52 +600,199 @@ namespace VNLib.Plugins.Essentials.SocialOauth
* The result is an encrypted nonce that should guard against replay attacks and MITM
*/
- private string BuildUrl(string base32Nonce, string pubKey, ReadOnlySpan<char> scheme, ReadOnlySpan<char> redirectAuthority, Encoding enc)
+ sealed class LoginUriBuilder
{
- //Char buffer for base32 and url building
- using UnsafeMemoryHandle<byte> buffer = MemoryUtil.UnsafeAlloc<byte>(8192, true);
- //get bin buffer slice
- Span<byte> binBuffer = buffer.Span[1024..];
- Span<byte> charBuffer = buffer.Span[..1024];
-
- ReadOnlySpan<char> url;
+ private readonly IMemoryHandle<byte> _buffer;
+
+ private Span<byte> _binBuffer => _buffer.Span[1024..];
+ private Span<char> _charBuffer => MemoryMarshal.Cast<byte, char>(_buffer.Span[..1024]);
+
+ private string? redirectUrl;
+ private string? nonce;
+ private Encoding _encoding;
+
+ private int _urlCharPointer;
+
+ public LoginUriBuilder()
+ {
+ //Alloc buffer
+ _buffer = MemoryUtil.SafeAllocNearestPage<byte>(8000, true);
+
+ //Set default encoding
+ _encoding = Encoding.UTF8;
+ }
+
+ public LoginUriBuilder WithUrl(ReadOnlySpan<char> scheme, ReadOnlySpan<char> authority, ReadOnlySpan<char> path)
{
- //Get char buffer slice and cast to char
- Span<char> charBuf = MemoryMarshal.Cast<byte, char>(charBuffer);
//buffer writer for easier syntax
- ForwardOnlyWriter<char> writer = new(charBuf);
+ ForwardOnlyWriter<char> writer = new(_charBuffer);
//first build the redirect url to re-encode it
writer.Append(scheme);
writer.Append("://");
//Create redirect url (current page, default action is to authorize the client)
- writer.Append(redirectAuthority);
- writer.Append(Path);
+ writer.Append(authority);
+ writer.Append(path);
//url encode the redirect path and save it for later
- string redirectFiltered = Uri.EscapeDataString(writer.ToString());
- //reset the writer again to begin building the path
- writer.Reset();
+ redirectUrl = Uri.EscapeDataString(writer.ToString());
+
+ return this;
+ }
+
+ public LoginUriBuilder WithEncoding(Encoding encoding)
+ {
+ _encoding = encoding;
+ return this;
+ }
+
+ public LoginUriBuilder WithNonce(string base32Nonce)
+ {
+ nonce = base32Nonce;
+ return this;
+ }
+
+ public LoginUriBuilder Build(OauthClientConfig config)
+ {
+ //buffer writer for easier syntax
+ ForwardOnlyWriter<char> writer = new(_charBuffer);
+
//Append the config redirect path
- writer.Append(Config.AccessCodeUrl.OriginalString);
+ writer.Append(config.AccessCodeUrl.OriginalString);
//begin query arguments
writer.Append("&client_id=");
- writer.Append(Config.ClientID);
+ writer.Append(config.ClientID);
//add the redirect url
writer.Append("&redirect_uri=");
- writer.Append(redirectFiltered);
+ writer.Append(redirectUrl);
//Append the state parameter
writer.Append("&state=");
- writer.Append(base32Nonce);
- url = writer.AsSpan();
+ writer.Append(nonce);
+
+ //Update url pointer
+ _urlCharPointer = writer.Written;
+
+ return this;
}
- //Separate buffers
- Span<byte> encryptionBuffer = binBuffer[1024..];
- Span<byte> encodingBuffer = binBuffer[..1024];
- //Encode the url to binary
- int byteCount = enc.GetBytes(url, encodingBuffer);
- //Encrypt the binary
- ERRNO count = AccountUtil.TryEncryptClientData(pubKey, encodingBuffer[..byteCount], in encryptionBuffer);
- //base64 encode the encrypted
- return Convert.ToBase64String(encryptionBuffer[0..(int)count]);
+
+ public string Encrypt(HttpEntity client, IClientSecInfo secInfo)
+ {
+ try
+ {
+ ReadOnlySpan<char> url = _charBuffer[.._urlCharPointer];
+
+ //Separate buffers
+ Span<byte> encryptionBuffer = _binBuffer[1024..];
+ Span<byte> encodingBuffer = _binBuffer[..1024];
+
+ //Encode the url to binary
+ int byteCount = _encoding.GetBytes(url, encodingBuffer);
+
+ //Encrypt the binary data
+ ERRNO count = client.TryEncryptClientData(secInfo, encodingBuffer[..byteCount], encryptionBuffer);
+
+ //base64 encode the encrypted
+ return Convert.ToBase64String(encryptionBuffer[0..(int)count]);
+ }
+ finally
+ {
+ _urlCharPointer = 0;
+ //Dispose buffer
+ _buffer.Dispose();
+ }
+ }
+
+ }
+
+ private static bool VerifyAndGetClaim(HttpEntity entity, [NotNullWhen(true)] out LoginClaim? claim)
+ {
+ claim = null;
+
+ //Try to get the cookie
+ if(!entity.Server.GetCookie(CLAIM_COOKIE_NAME, out string? cookieValue))
+ {
+ return false;
+ }
+
+ //Recover the signing key from the user's session
+ string sigKey = entity.Session[SESSION_SIG_KEY_NAME];
+ byte[]? key = VnEncoding.FromBase32String(sigKey);
+
+ if (key == null)
+ {
+ return false;
+ }
+
+ try
+ {
+ //Try to parse the jwt
+ using JsonWebToken jwt = JsonWebToken.Parse(cookieValue);
+
+ //Verify the jwt
+ using(HMAC alg = GetSigningAlg(key))
+ {
+ if (!jwt.Verify(alg))
+ {
+ return false;
+ }
+ }
+
+ //Recover the clam from the jwt
+ claim = jwt.GetPayload<LoginClaim>();
+
+ //Verify the expiration time
+ return claim.ExpirationSeconds > entity.RequestedTimeUtc.ToUnixTimeSeconds();
+ }
+ catch (FormatException)
+ {
+ return false;
+ }
+ finally
+ {
+ MemoryUtil.InitializeBlock(key.AsSpan());
+ }
+ }
+
+ private static void ClearClaimData(HttpEntity entity)
+ {
+ if (entity.Server.RequestCookies.ContainsKey(CLAIM_COOKIE_NAME))
+ {
+ entity.Server.ExpireCookie(CLAIM_COOKIE_NAME);
+ }
+
+ entity.Session[SESSION_SIG_KEY_NAME] = null!;
+ }
+
+ private void SignAndSetCookie(HttpEntity entity, LoginClaim claim)
+ {
+ //Setup Jwt
+ using JsonWebToken jwt = new();
+
+ //Write claim body
+ jwt.WritePayload(claim);
+
+ //Generate signing key
+ byte[] sigKey = RandomHash.GetRandomBytes(SIGNING_KEY_SIZE);
+
+ //Sign the jwt
+ using(HMAC alg = GetSigningAlg(sigKey))
+ {
+ jwt.Sign(alg);
+ }
+
+ //Build and set cookie
+ HttpCookie cookie = new(CLAIM_COOKIE_NAME, jwt.Compile())
+ {
+ Secure = true,
+ HttpOnly = true,
+ ValidFor = Config.InitClaimValidFor,
+ SameSite = CookieSameSite.SameSite
+ };
+
+ entity.Server.SetCookie(in cookie);
+
+ //Encode and store the signing key in the clien't session
+ entity.Session[SESSION_SIG_KEY_NAME] = VnEncoding.ToBase32String(sigKey);
+
+ MemoryUtil.InitializeBlock(sigKey.AsSpan());
}
}
}