aboutsummaryrefslogtreecommitdiff
path: root/plugins/VNLib.Plugins.Essentials.Auth.Social/src
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/VNLib.Plugins.Essentials.Auth.Social/src')
-rw-r--r--plugins/VNLib.Plugins.Essentials.Auth.Social/src/ClientAccessTokenState.cs48
-rw-r--r--plugins/VNLib.Plugins.Essentials.Auth.Social/src/ClientClaimManager.cs126
-rw-r--r--plugins/VNLib.Plugins.Essentials.Auth.Social/src/ClientRequestState.cs81
-rw-r--r--plugins/VNLib.Plugins.Essentials.Auth.Social/src/GetTokenRequest.cs34
-rw-r--r--plugins/VNLib.Plugins.Essentials.Auth.Social/src/IOAuthAccessState.cs55
-rw-r--r--plugins/VNLib.Plugins.Essentials.Auth.Social/src/IOAuthProvider.cs40
-rw-r--r--plugins/VNLib.Plugins.Essentials.Auth.Social/src/LoginClaim.cs73
-rw-r--r--plugins/VNLib.Plugins.Essentials.Auth.Social/src/LoginUriBuilder.cs127
-rw-r--r--plugins/VNLib.Plugins.Essentials.Auth.Social/src/OAuthSiteAdapter.cs73
-rw-r--r--plugins/VNLib.Plugins.Essentials.Auth.Social/src/OauthClientConfig.cs148
-rw-r--r--plugins/VNLib.Plugins.Essentials.Auth.Social/src/PortalsEndpoint.cs75
-rw-r--r--plugins/VNLib.Plugins.Essentials.Auth.Social/src/SocialAuthEntry.cs99
-rw-r--r--plugins/VNLib.Plugins.Essentials.Auth.Social/src/SocialOAuthPortal.cs34
-rw-r--r--plugins/VNLib.Plugins.Essentials.Auth.Social/src/SocialOauthBase.cs572
-rw-r--r--plugins/VNLib.Plugins.Essentials.Auth.Social/src/UserLoginData.cs34
-rw-r--r--plugins/VNLib.Plugins.Essentials.Auth.Social/src/VNLib.Plugins.Essentials.Auth.Social.csproj63
-rw-r--r--plugins/VNLib.Plugins.Essentials.Auth.Social/src/Validators/AccountDataValidator.cs83
-rw-r--r--plugins/VNLib.Plugins.Essentials.Auth.Social/src/Validators/LoginMessageValidation.cs65
18 files changed, 1830 insertions, 0 deletions
diff --git a/plugins/VNLib.Plugins.Essentials.Auth.Social/src/ClientAccessTokenState.cs b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/ClientAccessTokenState.cs
new file mode 100644
index 0000000..5ba77f2
--- /dev/null
+++ b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/ClientAccessTokenState.cs
@@ -0,0 +1,48 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Auth.Social
+* File: OAuthAccessState.cs
+*
+* OAuthAccessState.cs is part of VNLib.Plugins.Essentials.Auth.Social which
+* is part of the larger VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Essentials.Auth.Social 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.Auth.Social 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 System.Text.Json.Serialization;
+
+namespace VNLib.Plugins.Essentials.Auth.Social
+{
+ public class OAuthAccessState : IOAuthAccessState
+ {
+ ///<inheritdoc/>
+ [JsonPropertyName("access_token")]
+ public string? Token { get; set; }
+ ///<inheritdoc/>
+ [JsonPropertyName("scope")]
+ public string? Scope { get; set; }
+ ///<inheritdoc/>
+ [JsonPropertyName("token_type")]
+ public string? Type { get; set; }
+ ///<inheritdoc/>
+ [JsonPropertyName("refresh_token")]
+ public string? RefreshToken { get; set; }
+ ///<inheritdoc/>
+ [JsonPropertyName("id_token")]
+ public string? IdToken { get; set; }
+ }
+} \ No newline at end of file
diff --git a/plugins/VNLib.Plugins.Essentials.Auth.Social/src/ClientClaimManager.cs b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/ClientClaimManager.cs
new file mode 100644
index 0000000..0c4f9ba
--- /dev/null
+++ b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/ClientClaimManager.cs
@@ -0,0 +1,126 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Auth.Social
+* File: ClientClaimManager.cs
+*
+* ClientClaimManager.cs is part of VNLib.Plugins.Essentials.Auth.Social which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Essentials.Auth.Social 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.Auth.Social 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 System.Diagnostics.CodeAnalysis;
+
+using VNLib.Hashing;
+using VNLib.Hashing.IdentityUtility;
+using VNLib.Utils;
+using VNLib.Utils.Memory;
+using VNLib.Utils.Extensions;
+using VNLib.Plugins.Essentials.Accounts;
+using VNLib.Plugins.Essentials.Extensions;
+
+namespace VNLib.Plugins.Essentials.Auth.Social
+{
+ internal sealed record class ClientClaimManager(ICookieController Cookies)
+ {
+ const string SESSION_SIG_KEY_NAME = "soa.sig";
+ const int SIGNING_KEY_SIZE = 32;
+
+ public bool VerifyAndGetClaim(HttpEntity entity, [NotNullWhen(true)] out LoginClaim? claim)
+ {
+ claim = null;
+
+ string? cookieValue = Cookies.GetCookie(entity);
+
+ //Try to get the cookie
+ if (cookieValue == null)
+ {
+ return false;
+ }
+
+ //Recover the signing key from the user's session
+ string sigKey = entity.Session[SESSION_SIG_KEY_NAME];
+ Span<byte> key = stackalloc byte[SIGNING_KEY_SIZE + 16];
+
+ ERRNO keySize = VnEncoding.Base64UrlDecode(sigKey, key);
+
+ if (keySize < 1)
+ {
+ return false;
+ }
+
+ try
+ {
+ //Try to parse the jwt
+ using JsonWebToken jwt = JsonWebToken.Parse(cookieValue);
+
+ //Verify the jwt
+ if (!jwt.Verify(key[..(int)keySize], HashAlg.SHA256))
+ {
+ return false;
+ }
+
+ //Recover the clam from the jwt
+ claim = jwt.GetPayload<LoginClaim>();
+
+ //Verify the expiration time
+ return claim.ExpirationSeconds > entity.RequestedTimeUtc.ToUnixTimeSeconds();
+ }
+ catch (FormatException)
+ {
+ //JWT was corrupted and could not be parsed
+ return false;
+ }
+ finally
+ {
+ MemoryUtil.InitializeBlock(key);
+ }
+ }
+
+ public void ClearClaimData(HttpEntity entity)
+ {
+ //Remove the upgrade cookie
+ Cookies.ExpireCookie(entity, false);
+
+ //Clear the signing key from the session
+ entity.Session[SESSION_SIG_KEY_NAME] = null!;
+ }
+
+ public void SignAndSetCookie(HttpEntity entity, LoginClaim claim)
+ {
+ //Setup Jwt
+ using JsonWebToken jwt = new();
+
+ //Write claim body, we dont need a header
+ jwt.WritePayload(claim, Statics.SR_OPTIONS);
+
+ //Generate signing key
+ byte[] sigKey = RandomHash.GetRandomBytes(SIGNING_KEY_SIZE);
+
+ //Sign the jwt
+ jwt.Sign(sigKey, HashAlg.SHA256);
+
+ Cookies.SetCookie(entity, jwt.Compile());
+
+ //Encode and store the signing key in the clien't session
+ entity.Session[SESSION_SIG_KEY_NAME] = VnEncoding.ToBase64UrlSafeString(sigKey, false);
+
+ //Clear the signing key
+ MemoryUtil.InitializeBlock(sigKey.AsSpan());
+ }
+ }
+}
diff --git a/plugins/VNLib.Plugins.Essentials.Auth.Social/src/ClientRequestState.cs b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/ClientRequestState.cs
new file mode 100644
index 0000000..ea8eec9
--- /dev/null
+++ b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/ClientRequestState.cs
@@ -0,0 +1,81 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Auth.Social
+* File: ClientRequestState.cs
+*
+* ClientRequestState.cs is part of VNLib.Plugins.Essentials.Auth.Social which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Essentials.Auth.Social 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.Auth.Social 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 System.Security.Cryptography;
+
+using VNLib.Hashing;
+using VNLib.Utils;
+using VNLib.Utils.Memory;
+using VNLib.Utils.Memory.Caching;
+
+namespace VNLib.Plugins.Essentials.Auth.Social
+{
+ internal sealed class ClientRequestState : ICacheable
+ {
+ private readonly ReadOnlyMemory<byte> _rawKey;
+
+ /// <summary>
+ /// The raw nonce state bytes
+ /// </summary>
+ public ReadOnlyMemory<byte> State { get; private set; }
+
+ public ClientRequestState(ReadOnlySpan<char> keyChar, int nonceBytes)
+ {
+ //Get browser id
+ _rawKey = Convert.FromHexString(keyChar);
+ RecomputeState(nonceBytes);
+ }
+
+ /// <summary>
+ /// Recomputes a nonce state and signature for the current
+ /// connection
+ /// </summary>
+ /// <param name="nonceBytes">The size of the nonce (in bytes) to generate</param>
+ public void RecomputeState(int nonceBytes)
+ {
+ //Get random nonce buffer
+ State = RandomHash.GetRandomBytes(nonceBytes);
+ }
+ /// <summary>
+ /// Computes the signature of the supplied data based on the original
+ /// client state for this connection
+ /// </summary>
+ /// <param name="data"></param>
+ /// <returns></returns>
+ public ERRNO ComputeSignatureForClient(ReadOnlySpan<byte> data, Span<byte> output)
+ {
+ return HMACSHA512.TryHashData(_rawKey.Span, data, output, out int count) ? count : ERRNO.E_FAIL;
+ }
+
+ public DateTime Expires { get; set; }
+ bool IEquatable<ICacheable>.Equals(ICacheable other) => ReferenceEquals(this, other);
+ void ICacheable.Evicted()
+ {
+ //Zero secrets on eviction
+ MemoryUtil.UnsafeZeroMemory(State);
+ MemoryUtil.UnsafeZeroMemory(_rawKey);
+ }
+ }
+} \ No newline at end of file
diff --git a/plugins/VNLib.Plugins.Essentials.Auth.Social/src/GetTokenRequest.cs b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/GetTokenRequest.cs
new file mode 100644
index 0000000..d061937
--- /dev/null
+++ b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/GetTokenRequest.cs
@@ -0,0 +1,34 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Auth.Social
+* File: GetTokenRequest.cs
+*
+* GetTokenRequest.cs is part of VNLib.Plugins.Essentials.Auth.Social which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Essentials.Auth.Social 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.Auth.Social 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/.
+*/
+
+namespace VNLib.Plugins.Essentials.Auth.Social
+{
+ /// <summary>
+ /// A request message to get an OAuth2 token from a code
+ /// </summary>
+ /// <param name="Code">The clients authentication code</param>
+ /// <param name="RedirectUrl">The redirect url for current site</param>
+ public sealed record class GetTokenRequest(string Code, string RedirectUrl)
+ { }
+}
diff --git a/plugins/VNLib.Plugins.Essentials.Auth.Social/src/IOAuthAccessState.cs b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/IOAuthAccessState.cs
new file mode 100644
index 0000000..cbdd41a
--- /dev/null
+++ b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/IOAuthAccessState.cs
@@ -0,0 +1,55 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Auth.Social
+* File: IOAuthAccessState.cs
+*
+* IOAuthAccessState.cs is part of VNLib.Plugins.Essentials.Auth.Social which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Essentials.Auth.Social 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.Auth.Social 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/.
+*/
+
+namespace VNLib.Plugins.Essentials.Auth.Social
+{
+ /// <summary>
+ /// An object that represents an OAuth2 access token in its
+ /// standard form.
+ /// </summary>
+ public interface IOAuthAccessState
+ {
+ /// <summary>
+ /// The OAuth2 access token
+ /// </summary>
+ public string? Token { get; set; }
+ /// <summary>
+ /// Token grant scope
+ /// </summary>
+ string? Scope { get; set; }
+ /// <summary>
+ /// The OAuth2 token type, usually 'Bearer'
+ /// </summary>
+ string? Type { get; set; }
+ /// <summary>
+ /// Optional refresh token
+ /// </summary>
+ string? RefreshToken { get; set; }
+
+ /// <summary>
+ /// Optional ID OIDC token
+ /// </summary>
+ string? IdToken { get; set; }
+ }
+} \ No newline at end of file
diff --git a/plugins/VNLib.Plugins.Essentials.Auth.Social/src/IOAuthProvider.cs b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/IOAuthProvider.cs
new file mode 100644
index 0000000..6968e05
--- /dev/null
+++ b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/IOAuthProvider.cs
@@ -0,0 +1,40 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Auth.Social
+* File: IOAuthProvider.cs
+*
+* IOAuthProvider.cs is part of VNLib.Plugins.Essentials.Auth.Social which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Essentials.Auth.Social 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.Auth.Social 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/.
+*/
+
+namespace VNLib.Plugins.Essentials.Auth.Social
+{
+ /// <summary>
+ /// Represents a dynamically loaded social login provider
+ /// that exposes a set of social login portals
+ /// </summary>
+ public interface IOAuthProvider
+ {
+ /// <summary>
+ /// Gets all exported social login portals to be advertised
+ /// to clients
+ /// </summary>
+ /// <returns>The portal array</returns>
+ SocialOAuthPortal[] GetPortals();
+ }
+} \ No newline at end of file
diff --git a/plugins/VNLib.Plugins.Essentials.Auth.Social/src/LoginClaim.cs b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/LoginClaim.cs
new file mode 100644
index 0000000..70acff0
--- /dev/null
+++ b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/LoginClaim.cs
@@ -0,0 +1,73 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Auth.Social
+* File: LoginClaim.cs
+*
+* LoginClaim.cs is part of VNLib.Plugins.Essentials.Auth.Social which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Essentials.Auth.Social 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.Auth.Social 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.Text.Json.Serialization;
+
+using VNLib.Hashing;
+using VNLib.Utils;
+using VNLib.Utils.Memory;
+using VNLib.Plugins.Essentials.Accounts;
+
+namespace VNLib.Plugins.Essentials.Auth.Social
+{
+ internal sealed class LoginClaim : IClientSecInfo
+ {
+ [JsonPropertyName("exp")]
+ public long ExpirationSeconds { get; set; }
+
+ [JsonPropertyName("iat")]
+ public long IssuedAtTime { get; set; }
+
+ [JsonPropertyName("nonce")]
+ public string? Nonce { get; set; }
+
+ [JsonPropertyName("locallanguage")]
+ public string? LocalLanguage { get; set; }
+
+ [JsonPropertyName("pubkey")]
+ public string? PublicKey { get; set; }
+
+ [JsonPropertyName("clientid")]
+ public string? ClientId { get; set; }
+
+
+ public void ComputeNonce(int nonceSize)
+ {
+ //Alloc nonce buffer
+ using UnsafeMemoryHandle<byte> buffer = MemoryUtil.UnsafeAlloc(nonceSize);
+ try
+ {
+ //fill the buffer with random data
+ RandomHash.GetRandomBytes(buffer.Span);
+
+ //Base32-Encode nonce and save it
+ Nonce = VnEncoding.ToBase64UrlSafeString(buffer.Span, false);
+ }
+ finally
+ {
+ MemoryUtil.InitializeBlock(buffer.Span);
+ }
+ }
+ }
+}
diff --git a/plugins/VNLib.Plugins.Essentials.Auth.Social/src/LoginUriBuilder.cs b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/LoginUriBuilder.cs
new file mode 100644
index 0000000..da37fb7
--- /dev/null
+++ b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/LoginUriBuilder.cs
@@ -0,0 +1,127 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Auth.Social
+* File: LoginUriBuilder.cs
+*
+* LoginUriBuilder.cs is part of VNLib.Plugins.Essentials.Auth.Social which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Essentials.Auth.Social 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.Auth.Social 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 System.Text;
+using System.Runtime.InteropServices;
+
+using VNLib.Utils;
+using VNLib.Utils.Memory;
+using VNLib.Utils.Extensions;
+using VNLib.Plugins.Essentials.Accounts;
+
+namespace VNLib.Plugins.Essentials.Auth.Social
+{
+ /*
+ * 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
+ * target OAuth website.
+ *
+ * The result is an encrypted nonce that should guard against replay attacks and MITM
+ */
+
+ internal sealed record class LoginUriBuilder(OauthClientConfig Config)
+ {
+ private string? redirectUrl;
+ private string? nonce;
+ private Encoding _encoding = Encoding.UTF8;
+
+ public LoginUriBuilder WithUrl(ReadOnlySpan<char> scheme, ReadOnlySpan<char> authority, ReadOnlySpan<char> path)
+ {
+ //Alloc stack buffer for url
+ Span<char> buffer = stackalloc char[1024];
+
+ //buffer writer for easier syntax
+ ForwardOnlyWriter<char> writer = new(buffer);
+ //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(authority);
+ writer.Append(path);
+ //url encode the redirect path and save it for later
+ 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 string Encrypt(HttpEntity client, IClientSecInfo secInfo)
+ {
+ //Alloc buffer and split it into binary and char buffers
+ using UnsafeMemoryHandle<byte> buffer = MemoryUtil.UnsafeAllocNearestPage(8000);
+
+ Span<byte> binBuffer = buffer.Span[2048..];
+ Span<char> charBuffer = MemoryMarshal.Cast<byte, char>(buffer.Span[..2048]);
+
+
+ /*
+ * Build the character uri so we can encode it to binary,
+ * encrypt it and return it to the client
+ */
+
+ ForwardOnlyWriter<char> writer = new(charBuffer);
+
+ //Append the config redirect path
+ writer.Append(Config.AccessCodeUrl.OriginalString);
+ //begin query arguments
+ writer.Append("&client_id=");
+ writer.Append(Config.ClientID.Value);
+ //add the redirect url
+ writer.Append("&redirect_uri=");
+ writer.Append(redirectUrl);
+ //Append the state parameter
+ writer.Append("&state=");
+ writer.Append(nonce);
+
+ //Collect the written character data
+ ReadOnlySpan<char> url = writer.AsSpan();
+
+ //Separate bin buffers for encryption and encoding
+ 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]);
+ }
+ }
+}
diff --git a/plugins/VNLib.Plugins.Essentials.Auth.Social/src/OAuthSiteAdapter.cs b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/OAuthSiteAdapter.cs
new file mode 100644
index 0000000..37dd7e0
--- /dev/null
+++ b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/OAuthSiteAdapter.cs
@@ -0,0 +1,73 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Auth.Social
+* File: OAuthSiteAdapter.cs
+*
+* OAuthSiteAdapter.cs is part of VNLib.Plugins.Essentials.Auth.Social which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Essentials.Auth.Social 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.Auth.Social 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.Net;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+using RestSharp;
+
+using VNLib.Net.Rest.Client;
+using VNLib.Net.Rest.Client.Construction;
+
+namespace VNLib.Plugins.Essentials.Auth.Social
+{
+ /// <summary>
+ /// Provides strucutred http messaging to an OAuth2 site.
+ /// </summary>
+ public sealed class OAuthSiteAdapter : RestSiteAdapterBase
+ {
+ ///<inheritdoc/>
+ protected override RestClientPool Pool { get; }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OAuthSiteAdapter"/> class.
+ /// </summary>
+ public OAuthSiteAdapter()
+ {
+ RestClientOptions poolOptions = new()
+ {
+ MaxTimeout = 5000,
+ AutomaticDecompression = DecompressionMethods.All,
+ Encoding = Encoding.UTF8,
+ //disable redirects, api should not redirect
+ FollowRedirects = false,
+ };
+
+ //Configure rest client to comunications to main discord api
+ Pool = new(10, poolOptions);
+ }
+
+ ///<inheritdoc/>
+ public override void OnResponse(RestResponse response)
+ { }
+
+ ///<inheritdoc/>
+ public override Task WaitAsync(CancellationToken cancellation = default)
+ {
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/plugins/VNLib.Plugins.Essentials.Auth.Social/src/OauthClientConfig.cs b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/OauthClientConfig.cs
new file mode 100644
index 0000000..a3b43ad
--- /dev/null
+++ b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/OauthClientConfig.cs
@@ -0,0 +1,148 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Auth.Social
+* File: OauthClientConfig.cs
+*
+* OauthClientConfig.cs is part of VNLib.Plugins.Essentials.Auth.Social which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Essentials.Auth.Social 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.Auth.Social 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 System.Net;
+using System.Collections.Generic;
+
+using VNLib.Utils.Logging;
+using VNLib.Utils.Extensions;
+using VNLib.Plugins.Extensions.Loading;
+
+namespace VNLib.Plugins.Essentials.Auth.Social
+{
+
+ /// <summary>
+ /// Contains the standard configuration data for an OAuth2 endpoint
+ /// defined by plugin configuration
+ /// </summary>
+ public sealed class OauthClientConfig
+ {
+
+ public OauthClientConfig(PluginBase plugin, IConfigScope config)
+ {
+ EndpointPath = config["path"].GetString() ?? throw new KeyNotFoundException($"Missing required key 'path' in config {config.ScopeName}");
+
+ //Set discord account origin
+ AccountOrigin = config["account_origin"].GetString() ?? throw new KeyNotFoundException($"Missing required key 'account_origin' in config {config.ScopeName}");
+
+ //Get the auth and token urls
+ string authUrl = config["authorization_url"].GetString() ?? throw new KeyNotFoundException($"Missing required key 'authorization_url' in config {config.ScopeName}");
+ string tokenUrl = config["token_url"].GetString() ?? throw new KeyNotFoundException($"Missing required key 'token_url' in config {config.ScopeName}");
+ string userUrl = config["user_data_url"].GetString() ?? throw new KeyNotFoundException($"Missing required key 'user_data_url' in config {config.ScopeName}");
+ //Create the uris
+ AccessCodeUrl = new(authUrl);
+ AccessTokenUrl = new(tokenUrl);
+ UserDataUrl = new(userUrl);
+
+ AllowForLocalAccounts = config["allow_for_local"].GetBoolean();
+ AllowRegistration = config["allow_registration"].GetBoolean();
+ NonceByteSize = config["nonce_size"].GetUInt32();
+ RandomPasswordSize = config["password_size"].GetInt32();
+ InitClaimValidFor = config["claim_valid_for_sec"].GetTimeSpan(TimeParseType.Seconds);
+
+ //Setup async lazy loaders for secrets
+ ClientID = plugin.GetSecretAsync($"{config.ScopeName}_client_id")
+ .ToLazy(static r => r.Result.ToString());
+
+ ClientSecret = plugin.GetSecretAsync($"{config.ScopeName}_client_secret")
+ .ToLazy(static r => r.Result.ToString());
+
+ //Log the token server ip address for the user to verify
+ if (plugin.Log.IsEnabled(LogLevel.Verbose))
+ {
+ _ = plugin.ObserveWork(async () =>
+ {
+ IPAddress[] addresses = await Dns.GetHostAddressesAsync(AccessTokenUrl.DnsSafeHost);
+ plugin.Log.Verbose("Token server {host} resolves to {ip}", AccessTokenUrl.DnsSafeHost, addresses);
+ });
+ }
+ }
+
+ /// <summary>
+ /// The client ID for the OAuth2 service
+ /// </summary>
+ public IAsyncLazy<string> ClientID { get; }
+
+ /// <summary>
+ /// The client secret for the OAuth2 service
+ /// </summary>
+ public IAsyncLazy<string> ClientSecret { get; }
+
+
+ /// <summary>
+ /// The user-account origin value. Specifies that the user account
+ /// was created outside of the local account system
+ /// </summary>
+ public string AccountOrigin { get; }
+
+ /// <summary>
+ /// The URL to redirect the user to the OAuth2 service
+ /// to begin the authentication process
+ /// </summary>
+ public Uri AccessCodeUrl { get; }
+
+ /// <summary>
+ /// The remote endoint to exchange codes for access tokens
+ /// </summary>
+ public Uri AccessTokenUrl { get; }
+
+ /// <summary>
+ /// The endpoint to get user-data object from
+ /// </summary>
+ public Uri UserDataUrl { get; }
+
+ /// <summary>
+ /// The endpoint route/path
+ /// </summary>
+ public string EndpointPath { get; }
+
+ /// <summary>
+ /// The size (in bytes) of the random generated nonce
+ /// </summary>
+ public uint NonceByteSize { get; }
+
+ /// <summary>
+ /// A value that specifies if locally created accounts are allowed
+ /// to be logged in from an OAuth2 source
+ /// </summary>
+ public bool AllowForLocalAccounts { get; }
+
+ /// <summary>
+ /// A value that indicates if accounts that do not exist will be created
+ /// and logged in immediatly, on successfull OAuth2 flow
+ /// </summary>
+ public bool AllowRegistration { get; }
+
+ /// <summary>
+ /// The size (in bytes) of the random password generated for new users
+ /// </summary>
+ public int RandomPasswordSize { get; }
+
+ /// <summary>
+ /// The initial time the login claim is valid for
+ /// </summary>
+ public TimeSpan InitClaimValidFor { get; }
+ }
+}
diff --git a/plugins/VNLib.Plugins.Essentials.Auth.Social/src/PortalsEndpoint.cs b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/PortalsEndpoint.cs
new file mode 100644
index 0000000..4e5f867
--- /dev/null
+++ b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/PortalsEndpoint.cs
@@ -0,0 +1,75 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Auth.Social
+* File: PortalsEndpoint.cs
+*
+* PortalsEndpoint.cs is part of VNLib.Plugins.Essentials.Auth.Social which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Essentials.Auth.Social 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.Auth.Social 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 System.Linq;
+using System.Collections.Generic;
+
+using VNLib.Plugins.Essentials.Endpoints;
+using VNLib.Plugins.Extensions.Loading;
+
+
+namespace VNLib.Plugins.Essentials.Auth.Social
+{
+ [ConfigurationName("portals")]
+ internal sealed class PortalsEndpoint : UnprotectedWebEndpoint
+ {
+ private PortalDefJson[] _portals;
+
+ public PortalsEndpoint(PluginBase plugin, IConfigScope config)
+ {
+ string path = config.GetRequiredProperty("path", p => p.GetString()!);
+ InitPathAndLog(path, plugin.Log);
+
+ //Empty array by default
+ _portals = [];
+ }
+
+ public void SetPortals(IEnumerable<SocialOAuthPortal> portals)
+ {
+ //Convert to json
+ _portals = portals.Select(p => new PortalDefJson
+ {
+ id = p.PortalId,
+ login = p.LoginEndpoint.Path,
+ logout = p.LogoutEndpoint?.Path,
+ }).ToArray();
+ }
+
+ protected override VfReturnType Get(HttpEntity entity)
+ {
+ //return portals array as json
+ return VirtualOkJson(entity, _portals);
+ }
+
+ private sealed class PortalDefJson
+ {
+ public string? id { get; set; }
+
+ public string? login { get; set; }
+
+ public string? logout { get; set; }
+ }
+ }
+} \ No newline at end of file
diff --git a/plugins/VNLib.Plugins.Essentials.Auth.Social/src/SocialAuthEntry.cs b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/SocialAuthEntry.cs
new file mode 100644
index 0000000..397d688
--- /dev/null
+++ b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/SocialAuthEntry.cs
@@ -0,0 +1,99 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Auth.Social
+* File: SocialAuthEntry.cs
+*
+* SocialAuthEntry.cs is part of VNLib.Plugins.Essentials.Auth.Social which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Essentials.Auth.Social 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.Auth.Social 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 System.Linq;
+using System.Text.Json;
+using System.Collections.Generic;
+
+using VNLib.Utils.Logging;
+using VNLib.Plugins.Extensions.Loading;
+using VNLib.Plugins.Extensions.Loading.Routing;
+
+
+namespace VNLib.Plugins.Essentials.Auth.Social
+{
+
+ public sealed class SocialAuthEntry : PluginBase
+ {
+ const string ProviderConfigKey = "providers";
+
+ ///<inheritdoc/>
+ public override string PluginName => "Auth.Socal";
+
+ ///<inheritdoc/>
+ protected override void OnLoad()
+ {
+ Log.Information("Loading social authentication providers");
+
+ //Get provider array
+ if(PluginConfig.TryGetProperty(ProviderConfigKey, out JsonElement providerArray))
+ {
+ //Get dll file names
+ string[] providerDlls = providerArray.EnumerateArray()
+ .Select(e => e.GetString()!)
+ .ToArray();
+
+ List<SocialOAuthPortal> portals = new();
+
+ /*
+ * Using the loading library to create the exported services
+ * which are IOAuthProvider implementations
+ */
+ foreach (string dll in providerDlls)
+ {
+ //Load the dll
+ IOAuthProvider provider = this.CreateServiceExternal<IOAuthProvider>(dll);
+
+ //Capture all portals
+ portals.AddRange(provider.GetPortals());
+
+ Log.Information($"Loaded OAuth method {provider.GetType().Name}");
+ }
+
+ //Define portals endpoint and set portals
+ PortalsEndpoint p = this.Route<PortalsEndpoint>();
+ p.SetPortals(portals);
+ }
+ else
+ {
+ Log.Warn("No providers array defined in config");
+ }
+ }
+
+ ///<inheritdoc/>
+ protected override void OnUnLoad()
+ {
+ Log.Information("Plugin unloaded");
+ }
+
+ ///<inheritdoc/>
+ protected override void ProcessHostCommand(string cmd)
+ {
+ throw new NotImplementedException();
+ }
+
+
+ }
+} \ No newline at end of file
diff --git a/plugins/VNLib.Plugins.Essentials.Auth.Social/src/SocialOAuthPortal.cs b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/SocialOAuthPortal.cs
new file mode 100644
index 0000000..3fe6ddf
--- /dev/null
+++ b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/SocialOAuthPortal.cs
@@ -0,0 +1,34 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Auth.Social
+* File: SocialOAuthPortal.cs
+*
+* SocialOAuthPortal.cs is part of VNLib.Plugins.Essentials.Auth.Social which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Essentials.Auth.Social 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.Auth.Social 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/.
+*/
+
+namespace VNLib.Plugins.Essentials.Auth.Social
+{
+ /// <summary>
+ /// Defines a single oauth social login portal
+ /// </summary>
+ /// <param name="PortalId"> The unique identifier for the portal </param>
+ /// <param name="LoginEndpoint"> Required login endpoint to advertise to the client </param>
+ /// <param name="LogoutEndpoint"> Optional logout endpoint to advertise to the client </param>
+ public record SocialOAuthPortal(string PortalId, IEndpoint LoginEndpoint, IEndpoint? LogoutEndpoint);
+} \ No newline at end of file
diff --git a/plugins/VNLib.Plugins.Essentials.Auth.Social/src/SocialOauthBase.cs b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/SocialOauthBase.cs
new file mode 100644
index 0000000..52da637
--- /dev/null
+++ b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/SocialOauthBase.cs
@@ -0,0 +1,572 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Auth.Social
+* File: SocialOauthBase.cs
+*
+* SocialOauthBase.cs is part of VNLib.Plugins.Essentials.Auth.Social which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Essentials.Auth.Social 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.Auth.Social 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 System.Net;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Security.Cryptography;
+using System.Text.Json.Serialization;
+
+using FluentValidation;
+
+using RestSharp;
+
+using VNLib.Net.Http;
+using VNLib.Utils;
+using VNLib.Utils.Logging;
+using VNLib.Utils.Extensions;
+using VNLib.Net.Rest.Client.Construction;
+using VNLib.Plugins.Essentials.Users;
+using VNLib.Plugins.Essentials.Accounts;
+using VNLib.Plugins.Essentials.Endpoints;
+using VNLib.Plugins.Essentials.Extensions;
+using VNLib.Plugins.Essentials.Auth.Social.Validators;
+using VNLib.Plugins.Extensions.Loading;
+using VNLib.Plugins.Extensions.Validation;
+using VNLib.Plugins.Extensions.Loading.Users;
+
+using ContentType = VNLib.Net.Http.ContentType;
+
+namespace VNLib.Plugins.Essentials.Auth.Social
+{
+
+ /// <summary>
+ /// Provides a base class for derriving commong OAuth2 implicit authentication
+ /// </summary>
+ public abstract class SocialOauthBase : UnprotectedWebEndpoint
+ {
+ const string AUTH_ERROR_MESSAGE = "You have no pending authentication requests.";
+
+ const string AUTH_GRANT_SESSION_NAME = "auth";
+ const string SESSION_TOKEN_KEY_NAME = "soa.tkn";
+ const string CLAIM_COOKIE_NAME = "extern-claim";
+
+
+ /// <summary>
+ /// The client configuration struct passed during base class construction
+ /// </summary>
+ protected virtual OauthClientConfig Config { get; }
+
+ ///<inheritdoc/>
+ protected override ProtectionSettings EndpointProtectionSettings { get; }
+
+ /// <summary>
+ /// The site adapter used to make requests to the OAuth2 provider
+ /// </summary>
+ protected OAuthSiteAdapter SiteAdapter { get; }
+
+ /// <summary>
+ /// The user manager used to create and manage user accounts
+ /// </summary>
+ protected IUserManager Users { get; }
+
+ private readonly IValidator<LoginClaim> ClaimValidator;
+ private readonly IValidator<string> NonceValidator;
+ private readonly IValidator<AccountData> AccountDataValidator;
+ private readonly ClientClaimManager _claims;
+
+ protected SocialOauthBase(PluginBase plugin, IConfigScope config)
+ {
+ ClaimValidator = GetClaimValidator();
+ NonceValidator = GetNonceValidator();
+ AccountDataValidator = new AccountDataValidator();
+
+ //Get the configuration element for the derrived type
+ Config = plugin.CreateService<OauthClientConfig>(config);
+
+ //Init endpoint
+ InitPathAndLog(Config.EndpointPath, plugin.Log);
+
+ Users = plugin.GetOrCreateSingleton<UserManager>();
+
+
+ //Setup cookie controller and claim manager
+ SingleCookieController cookies = new(CLAIM_COOKIE_NAME, Config.InitClaimValidFor)
+ {
+ Secure = true,
+ HttpOnly = true,
+ SameSite = CookieSameSite.None,
+ Path = Path
+ };
+
+ _claims = new(cookies);
+
+ //Define the site adapter
+ SiteAdapter = new();
+
+ //Define the the get-token request endpoint
+ SiteAdapter.DefineSingleEndpoint()
+ .WithEndpoint<GetTokenRequest>()
+ .WithMethod(Method.Post)
+ .WithUrl(Config.AccessTokenUrl)
+ .WithHeader("Accept", HttpHelpers.GetContentTypeString(ContentType.Json))
+ .WithParameter("client_id", c => Config.ClientID.Value)
+ .WithParameter("client_secret", c => Config.ClientSecret.Value)
+ .WithParameter("grant_type", "authorization_code")
+ .WithParameter("code", r => r.Code)
+ .WithParameter("redirect_uri", r => r.RedirectUrl);
+ }
+
+ private static IValidator<LoginClaim> GetClaimValidator()
+ {
+ InlineValidator<LoginClaim> val = new();
+ val.RuleFor(static s => s.ClientId)
+ .Length(10, 100)
+ .WithMessage("Request is not valid")
+ .AlphaNumericOnly()
+ .WithMessage("Request is not valid");
+
+ val.RuleFor(static s => s.PublicKey)
+ .Length(50, 1024)
+ .WithMessage("Request is not valid");
+
+ val.RuleFor(static s => s.LocalLanguage)
+ .Length(2, 10)
+ .WithMessage("Request is not valid");
+
+ return val;
+ }
+
+ private static IValidator<string> GetNonceValidator()
+ {
+ InlineValidator<string> val = new();
+ val.RuleFor(static s => s)
+ .Length(10, 200)
+ //Nonces are base32, so only alpha num
+ .AlphaNumeric();
+ return val;
+ }
+
+ ///<inheritdoc/>
+ protected override ERRNO PreProccess(HttpEntity entity)
+ {
+ if (!base.PreProccess(entity))
+ {
+ return false;
+ }
+
+ /*
+ * Cross site checking is disabled because we need to allow cross site
+ * for OAuth2 redirect flows
+ */
+ if (entity.Server.Method != HttpMethod.GET && entity.Server.IsCrossSite())
+ {
+ return false;
+ }
+
+ //Make sure the user is not logged in
+ return !entity.IsClientAuthorized(AuthorzationCheckLevel.Any);
+ }
+
+ /// <summary>
+ /// When derrived in a child class, exchanges an OAuth2 code grant type
+ /// for an OAuth2 access token to make api requests
+ /// </summary>
+ /// <param name="ev"></param>
+ /// <param name="code">The raw code from the remote OAuth2 granting server</param>
+ /// <param name="cancellationToken">A token to cancel the operation</param>
+ /// <returns>
+ /// 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>
+ protected virtual async Task<OAuthAccessState?> ExchangeCodeForTokenAsync(HttpEntity ev, string code, CancellationToken cancellationToken)
+ {
+ //Create new request object
+ GetTokenRequest req = new(code, $"{ev.Server.RequestUri.Scheme}://{ev.Server.RequestUri.Authority}{Path}");
+
+ //Execute request and attempt to recover the authorization response
+ Oauth2TokenResult? response = await SiteAdapter.ExecuteAsync(req, cancellationToken).AsJson<Oauth2TokenResult>();
+
+ if(response?.Error != null)
+ {
+ Log.Debug("Error result from {conf} code {code} description: {err}", Config.AccountOrigin, response.Error, response.ErrorDescription);
+ return null;
+ }
+
+ return response;
+ }
+
+ /// <summary>
+ /// Gets an object that represents the user's account data from the OAuth provider when
+ /// creating a new user for the current platform
+ /// </summary>
+ /// <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>
+ 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)
+ /// </summary>
+ /// <param name="clientAccess">The authorization information granted from the OAuth2 authorization server</param>
+ /// <param name="cancellation">A token to cancel the operation</param>
+ /// <returns></returns>
+ protected abstract Task<UserLoginData?> GetLoginDataAsync(IOAuthAccessState clientAccess, CancellationToken cancellation);
+
+
+
+ /*
+ * 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
+ */
+
+ ///<inheritdoc/>
+ 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;
+ }
+
+ //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(Config)
+ .WithEncoding(entity.Server.Encoding)
+ .WithUrl(entity.Server.RequestUri.Scheme, entity.Server.RequestUri.Authority, Path)
+ .WithNonce(claim.Nonce!)
+ .Encrypt(entity, claim);
+
+ //Sign and set the claim cookie
+ _claims.SignAndSetCookie(entity, claim);
+
+ webm.Success = true;
+ //Response
+ return VirtualOk(entity, webm);
+ }
+
+ /*
+ * Get method is invoked when the remote OAuth2 control has been passed back
+ * to this server. If successful should include a code that grants authorization
+ * and include a state variable that the client decrypted from an initial claim
+ * to prove its identity
+ */
+
+ ///<inheritdoc/>
+ protected override async ValueTask<VfReturnType> GetAsync(HttpEntity entity)
+ {
+ //Make sure state and code parameters are available
+ if (entity.QueryArgs.TryGetNonEmptyValue("state", out string? state)
+ && entity.QueryArgs.TryGetNonEmptyValue("code", out string? code))
+ {
+ //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())
+ {
+ _claims.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 (!_claims.VerifyAndGetClaim(entity, out LoginClaim? claim))
+ {
+ _claims.ClearClaimData(entity);
+ entity.Redirect(RedirectType.Temporary, $"{Path}?result=expired");
+ return VfReturnType.VirtualSkip;
+ }
+
+ //Confirm the nonce matches the claim
+ if (string.CompareOrdinal(claim.Nonce, state) != 0)
+ {
+ _claims.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)
+ {
+ _claims.ClearClaimData(entity);
+ entity.Redirect(RedirectType.Temporary, $"{Path}?result=invalid");
+ return VfReturnType.VirtualSkip;
+ }
+
+ //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
+ _claims.SignAndSetCookie(entity, claim);
+
+ //Prepare redirect
+ 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))
+ {
+ _claims.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;
+ }
+
+ return VfReturnType.ProcessAsFile;
+ }
+
+ /*
+ * Post messages finalize a login from a nonce
+ */
+
+ ///<inheritdoc/>
+ protected override async ValueTask<VfReturnType> PostAsync(HttpEntity entity)
+ {
+ ValErrWebMessage webm = new();
+
+ //Get the finalization message
+ using JsonDocument? request = await entity.GetJsonFromFileAsync();
+
+ if (webm.Assert(request != null, "Request message is required"))
+ {
+ return VirtualClose(entity, webm, HttpStatusCode.BadRequest);
+ }
+
+ //Recover the nonce
+ string? base32Nonce = request.RootElement.GetPropString("nonce");
+
+ if(webm.Assert(base32Nonce != null, "Nonce parameter is required"))
+ {
+ return VirtualClose(entity, webm, HttpStatusCode.UnprocessableEntity);
+ }
+
+ //Validate nonce
+ if (!NonceValidator.Validate(base32Nonce, webm))
+ {
+ return VirtualClose(entity, webm, HttpStatusCode.UnprocessableEntity);
+ }
+
+ //Recover the access token
+ if (webm.Assert(_claims.VerifyAndGetClaim(entity, out LoginClaim? claim), AUTH_ERROR_MESSAGE))
+ {
+ return VirtualOk(entity, webm);
+ }
+
+ //We can clear the client's access claim
+ _claims.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))
+ {
+ return VirtualOk(entity, webm);
+ }
+
+ //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);
+
+ if(webm.Assert(userLogin?.UserId != null, AUTH_ERROR_MESSAGE))
+ {
+ return VirtualOk(entity, webm);
+ }
+
+ //Convert the platform user-id to a database-safe user-id
+ string computedId = Users.ComputeSafeUserId(userLogin.UserId!);
+
+ //Fetch the user from the database
+ IUser? user = await Users.GetUserFromIDAsync(computedId, entity.EventCancellation);
+
+ /*
+ * If a user is not found, we can optionally create a new user account
+ * if the configuration allows it.
+ */
+ if(user == null)
+ {
+ //make sure registration is enabled
+ if (webm.Assert(Config.AllowRegistration, AUTH_ERROR_MESSAGE))
+ {
+ return VirtualOk(entity, webm);
+ }
+
+ //Get the clients personal info to being login process
+ AccountData? userAccount = await GetAccountDataAsync(token, entity.EventCancellation);
+
+ if (webm.Assert(userAccount != null, AUTH_ERROR_MESSAGE))
+ {
+ return VirtualOk(entity, webm);
+ }
+
+ //Validate the account data
+ if (webm.Assert(AccountDataValidator.Validate(userAccount).IsValid, AUTH_ERROR_MESSAGE))
+ {
+ return VirtualOk(entity, webm);
+ }
+
+ //See if user by email address exists
+ user = await Users.GetUserFromEmailAsync(userAccount.EmailAddress!, entity.EventCancellation);
+
+ if (user == null)
+ {
+ //Create the new user account
+ UserCreationRequest creation = new()
+ {
+ EmailAddress = userAccount.EmailAddress!,
+ InitialStatus = UserStatus.Active,
+ };
+
+ try
+ {
+ //Create the user with the specified email address, minimum privilage level, and an empty password
+ user = await Users.CreateUserAsync(creation, computedId, entity.EventCancellation);
+
+ //Store the new profile and origin
+ user.SetProfile(userAccount);
+ user.SetAccountOrigin(Config.AccountOrigin);
+ }
+ catch (UserCreationFailedException)
+ {
+ Log.Warn("Failed to create new user from new OAuth2 login, because a creation exception occured");
+ webm.Result = "Please try again later";
+ return VirtualOk(entity, webm);
+ }
+
+ //Skip check since we just created the user
+ goto Authorize;
+ }
+
+ /*
+ * User account already exists via email address but not
+ * user-id
+ */
+ }
+
+ //Make sure local accounts are allowed
+ if (webm.Assert(!user.IsLocalAccount() || Config.AllowForLocalAccounts, AUTH_ERROR_MESSAGE))
+ {
+ return VirtualOk(entity, webm);
+ }
+
+ //Reactivate inactive accounts
+ if (user.Status == UserStatus.Inactive)
+ {
+ user.Status = UserStatus.Active;
+ }
+
+ //Make sure the account is active
+ if (webm.Assert(user.Status == UserStatus.Active, AUTH_ERROR_MESSAGE))
+ {
+ return VirtualOk(entity, webm);
+ }
+
+ Authorize:
+
+ try
+ {
+ //Generate authoization
+ 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
+ Log.Debug("Successful social login for user {uid}... from {ip}", user.UserID[..8], entity.TrustedRemoteIp);
+
+ //release the user
+ await user.ReleaseAsync();
+ }
+ catch (CryptographicException ce)
+ {
+ Log.Debug("Failed to generate authorization for {user}, error {err}", user.UserID, ce.Message);
+ webm.Result = AUTH_ERROR_MESSAGE;
+ }
+ catch (OutOfMemoryException)
+ {
+ Log.Debug("Out of buffer space for token data encryption, for user {usr}, from ip {ip}", user.UserID, entity.TrustedRemoteIp);
+ webm.Result = AUTH_ERROR_MESSAGE;
+ }
+ catch(UserUpdateException uue)
+ {
+ webm.Token = null;
+ webm.Result = AUTH_ERROR_MESSAGE;
+ webm.Success = false;
+
+ //destroy any login data on failure
+ entity.InvalidateLogin();
+
+ Log.Error("Failed to update the user's account cause:\n{err}",uue);
+ }
+ finally
+ {
+ user.Dispose();
+ }
+ return VirtualOk(entity, webm);
+ }
+
+
+ sealed class Oauth2TokenResult: OAuthAccessState
+ {
+ [JsonPropertyName("error")]
+ public string? Error { get; set; }
+
+ [JsonPropertyName("error_description")]
+ public string? ErrorDescription { get; set; }
+ }
+
+ }
+}
diff --git a/plugins/VNLib.Plugins.Essentials.Auth.Social/src/UserLoginData.cs b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/UserLoginData.cs
new file mode 100644
index 0000000..7451539
--- /dev/null
+++ b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/UserLoginData.cs
@@ -0,0 +1,34 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Auth.Social
+* File: UserLoginData.cs
+*
+* UserLoginData.cs is part of VNLib.Plugins.Essentials.Auth.Social which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Essentials.Auth.Social 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.Auth.Social 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.Text.Json.Serialization;
+
+namespace VNLib.Plugins.Essentials.Auth.Social
+{
+ public class UserLoginData
+ {
+ [JsonPropertyName("user_id")]
+ public string? UserId { get; set; }
+ }
+}
diff --git a/plugins/VNLib.Plugins.Essentials.Auth.Social/src/VNLib.Plugins.Essentials.Auth.Social.csproj b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/VNLib.Plugins.Essentials.Auth.Social.csproj
new file mode 100644
index 0000000..dd6132c
--- /dev/null
+++ b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/VNLib.Plugins.Essentials.Auth.Social.csproj
@@ -0,0 +1,63 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Nullable>enable</Nullable>
+ <TargetFramework>net8.0</TargetFramework>
+ <RootNamespace>VNLib.Plugins.Essentials.Auth.Social</RootNamespace>
+ <AssemblyName>Auth.Social</AssemblyName>
+ <GenerateDocumentationFile>True</GenerateDocumentationFile>
+ <AnalysisLevel>latest-all</AnalysisLevel>
+ <NeutralLanguage>en-US</NeutralLanguage>
+ <EnableDynamicLoading>true</EnableDynamicLoading>
+ </PropertyGroup>
+
+ <PropertyGroup>
+ <PackageId>VNLib.Plugins.Essentials.Auth.Social</PackageId>
+ <Authors>Vaughn Nugent</Authors>
+ <Company>Vaughn Nugent</Company>
+ <Product>A shared library for building social OAuth authentication endpoints.</Product>
+ <Copyright>Copyright © 2024 Vaughn Nugent</Copyright>
+ <PackageProjectUrl>https://www.vaughnnugent.com/resources/software/modules/Plugins.Essentials</PackageProjectUrl>
+ <RepositoryUrl>https://github.com/VnUgE/Plugins.Essentials/tree/master/plugins/VNLib.Plugins.Essentials.Auth.Social</RepositoryUrl>
+ <Description>Essentials framework plugin that loads dynamic providers like my packaged GitHub, Discord, and Auth0 providers. This package also contains abstractions for building other providers. and may be inlcuded as a library to build a new provider</Description>
+ </PropertyGroup>
+
+ <PropertyGroup>
+ <PackageReadmeFile>README.md</PackageReadmeFile>
+ <PackageLicenseFile>LICENSE</PackageLicenseFile>
+ </PropertyGroup>
+ <ItemGroup>
+ <None Include="..\..\..\LICENSE">
+ <Pack>True</Pack>
+ <PackagePath>\</PackagePath>
+ <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+ </None>
+ <None Include="..\README.md">
+ <Pack>True</Pack>
+ <PackagePath>\</PackagePath>
+ </None>
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="ErrorProne.NET.CoreAnalyzers" Version="0.1.2">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ <PackageReference Include="ErrorProne.NET.Structs" Version="0.1.2">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\..\..\..\core\lib\Net.Rest.Client\src\VNLib.Net.Rest.Client.csproj" />
+ <ProjectReference Include="..\..\..\..\..\core\lib\Plugins.Essentials\src\VNLib.Plugins.Essentials.csproj" />
+ <ProjectReference Include="..\..\..\..\Extensions\lib\VNLib.Plugins.Extensions.Loading\src\VNLib.Plugins.Extensions.Loading.csproj" />
+ <ProjectReference Include="..\..\..\..\Extensions\lib\VNLib.Plugins.Extensions.Validation\src\VNLib.Plugins.Extensions.Validation.csproj" />
+ </ItemGroup>
+
+ <Target Condition="'$(BuildingInsideVisualStudio)' == true" Name="PostBuild" AfterTargets="PostBuildEvent">
+ <Exec Command="start xcopy &quot;$(TargetDir)&quot; &quot;$(SolutionDir)devplugins\$(TargetName)&quot; /E /Y /R" />
+ </Target>
+
+</Project>
diff --git a/plugins/VNLib.Plugins.Essentials.Auth.Social/src/Validators/AccountDataValidator.cs b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/Validators/AccountDataValidator.cs
new file mode 100644
index 0000000..269a9f3
--- /dev/null
+++ b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/Validators/AccountDataValidator.cs
@@ -0,0 +1,83 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Auth.Social
+* File: AccountDataValidator.cs
+*
+* AccountDataValidator.cs is part of VNLib.Plugins.Essentials.Auth.Social which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Essentials.Auth.Social 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.Auth.Social 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 FluentValidation;
+
+using VNLib.Plugins.Essentials.Accounts;
+using VNLib.Plugins.Extensions.Validation;
+
+namespace VNLib.Plugins.Essentials.Auth.Social.Validators
+{
+ internal class AccountDataValidator : AbstractValidator<AccountData>
+ {
+ public AccountDataValidator() : base()
+ {
+ RuleFor(t => t.EmailAddress)
+ .NotEmpty()
+ .WithMessage("Your account does not have an email address assigned to it");
+
+ RuleFor(t => t.City)
+ .MaximumLength(35)
+ .AlphaOnly()
+ .When(t => t.City?.Length > 0);
+
+ RuleFor(t => t.Company)
+ .MaximumLength(50)
+ .SpecialCharacters()
+ .When(t => t.Company?.Length > 0);
+
+ //Require a first and last names to be set together
+ When(t => t.First?.Length > 0 || t.Last?.Length > 0, () =>
+ {
+ RuleFor(t => t.First)
+ .Length(1, 35)
+ .AlphaOnly();
+ RuleFor(t => t.Last)
+ .Length(1, 35)
+ .AlphaOnly();
+ });
+
+ RuleFor(t => t.PhoneNumber)
+ .PhoneNumber()
+ .When(t => t.PhoneNumber?.Length > 0)
+ .OverridePropertyName("Phone");
+
+ //State must be 2 characters for us states if set
+ RuleFor(t => t.State)
+ .Length(2)
+ .When(t => t.State?.Length > 0);
+
+ RuleFor(t => t.Street)
+ .AlphaNumericOnly()
+ .MaximumLength(50)
+ .When(t => t.Street?.Length > 0);
+
+ //Allow empty zip codes, but if one is defined, is must be less than 7 characters
+ RuleFor(t => t.Zip)
+ .NumericOnly()
+ .MaximumLength(7)
+ .When(t => t.Zip?.Length > 0);
+ }
+ }
+}
diff --git a/plugins/VNLib.Plugins.Essentials.Auth.Social/src/Validators/LoginMessageValidation.cs b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/Validators/LoginMessageValidation.cs
new file mode 100644
index 0000000..f3894c9
--- /dev/null
+++ b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/Validators/LoginMessageValidation.cs
@@ -0,0 +1,65 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Auth.Social
+* File: LoginMessageValidation.cs
+*
+* LoginMessageValidation.cs is part of VNLib.Plugins.Essentials.Auth.Social which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Essentials.Auth.Social 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.Auth.Social 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 FluentValidation;
+
+using VNLib.Plugins.Essentials.Accounts;
+using VNLib.Plugins.Extensions.Validation;
+
+namespace VNLib.Plugins.Essentials.Auth.Social.Validators
+{
+ internal class LoginMessageValidation : AbstractValidator<LoginMessage>
+ {
+ /*
+ * A login message object is only used for common semantics within
+ * the user-system so validation operations are different than a
+ * normal login endpoint as named fields may be used differently
+ */
+ public LoginMessageValidation()
+ {
+ RuleFor(t => t.ClientId)
+ .Length(10, 50)
+ .WithMessage("Your browser is not sending required security information")
+ .IllegalCharacters()
+ .WithMessage("Your browser is not sending required security information");
+
+ RuleFor(t => t.ClientPublicKey)
+ .Length (50, 1000)
+ .WithMessage("Your browser is not sending required security information")
+ .IllegalCharacters()
+ .WithMessage("Your browser is not sending required security information");
+
+ //Password is only used for nonce tokens
+ RuleFor(t => t.Password).NotEmpty();
+
+ RuleFor(t => t.LocalLanguage)
+ .NotEmpty()
+ .WithMessage("Your language is not supported")
+ .AlphaNumericOnly()
+ .WithMessage("Your language is not supported");
+ }
+ }
+}