aboutsummaryrefslogtreecommitdiff
path: root/plugins
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2024-01-06 18:06:01 -0500
committerLibravatar vnugent <public@vaughnnugent.com>2024-01-06 18:06:01 -0500
commit3bd7effc15d0b87adce01281b073aa1db67d3cba (patch)
treee8fcf15b9d6664bcd48bb17ac2c71c70abda204d /plugins
parentf4c2c9e148374f462592c19e8ffd4db14672805d (diff)
social portal conversion, pull provider libraries & include some prebuilts
Diffstat (limited to 'plugins')
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/VNLib.Plugins.Essentials.Accounts.Registration.csproj2
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts/src/VNLib.Plugins.Essentials.Accounts.csproj2
-rw-r--r--plugins/VNLib.Plugins.Essentials.Auth.Social/README.md18
-rw-r--r--plugins/VNLib.Plugins.Essentials.Auth.Social/build.readme.md0
-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
-rw-r--r--plugins/VNLib.Plugins.Essentials.Content.Routing/src/VNLib.Plugins.Essentials.Content.Routing.csproj4
-rw-r--r--plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/README.md15
-rw-r--r--plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/build.readme.md11
-rw-r--r--plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/src/Auth0Portal.cs67
-rw-r--r--plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/src/Endpoints/LoginEndpoint.cs191
-rw-r--r--plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/src/Endpoints/LogoutEndpoint.cs70
-rw-r--r--plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/src/VNLib.Plugins.Essentials.Auth.Auth0.csproj48
-rw-r--r--plugins/providers/VNLib.Plugins.Essentials.Auth.Discord/README.md15
-rw-r--r--plugins/providers/VNLib.Plugins.Essentials.Auth.Discord/build.readme.md11
-rw-r--r--plugins/providers/VNLib.Plugins.Essentials.Auth.Discord/src/DiscordPortal.cs65
-rw-r--r--plugins/providers/VNLib.Plugins.Essentials.Auth.Discord/src/Endpoint/DiscordOauth.cs148
-rw-r--r--plugins/providers/VNLib.Plugins.Essentials.Auth.Discord/src/VNLib.Plugins.Essentials.Auth.Discord.csproj48
-rw-r--r--plugins/providers/VNLib.Plugins.Essentials.Auth.Github/README.md15
-rw-r--r--plugins/providers/VNLib.Plugins.Essentials.Auth.Github/build.readme.md11
-rw-r--r--plugins/providers/VNLib.Plugins.Essentials.Auth.Github/src/Endpoint/GitHubOauth.cs213
-rw-r--r--plugins/providers/VNLib.Plugins.Essentials.Auth.Github/src/GithubPortal.cs65
-rw-r--r--plugins/providers/VNLib.Plugins.Essentials.Auth.Github/src/VNLib.Plugins.Essentials.Auth.Github.csproj48
39 files changed, 2893 insertions, 4 deletions
diff --git a/plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/VNLib.Plugins.Essentials.Accounts.Registration.csproj b/plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/VNLib.Plugins.Essentials.Accounts.Registration.csproj
index 1f003a2..c703461 100644
--- a/plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/VNLib.Plugins.Essentials.Accounts.Registration.csproj
+++ b/plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/VNLib.Plugins.Essentials.Accounts.Registration.csproj
@@ -56,7 +56,7 @@
</ItemGroup>
<Target Condition="'$(BuildingInsideVisualStudio)' == true" Name="PostBuild" AfterTargets="PostBuildEvent">
- <Exec Command="start xcopy &quot;$(TargetDir)&quot; &quot;..\..\..\..\..\devplugins\$(TargetName)&quot; /E /Y /R" />
+ <Exec Command="start xcopy &quot;$(TargetDir)&quot; &quot;$(SolutionDir)devplugins\$(TargetName)&quot; /E /Y /R" />
</Target>
</Project>
diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/VNLib.Plugins.Essentials.Accounts.csproj b/plugins/VNLib.Plugins.Essentials.Accounts/src/VNLib.Plugins.Essentials.Accounts.csproj
index f8f7083..6faa14d 100644
--- a/plugins/VNLib.Plugins.Essentials.Accounts/src/VNLib.Plugins.Essentials.Accounts.csproj
+++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/VNLib.Plugins.Essentials.Accounts.csproj
@@ -60,7 +60,7 @@
</ItemGroup>
<Target Condition="'$(BuildingInsideVisualStudio)' == true" Name="PostBuild" AfterTargets="PostBuildEvent">
- <Exec Command="start xcopy &quot;$(TargetDir)&quot; &quot;..\..\..\..\..\devplugins\$(TargetName)&quot; /E /Y /R" />
+ <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/README.md b/plugins/VNLib.Plugins.Essentials.Auth.Social/README.md
new file mode 100644
index 0000000..9b7a992
--- /dev/null
+++ b/plugins/VNLib.Plugins.Essentials.Auth.Social/README.md
@@ -0,0 +1,18 @@
+# VNLib.Plugins.Essentials.Auth.Social
+*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 providers.
+
+## Builds
+Debug build w/ symbols & xml docs, release builds, NuGet packages, and individually packaged source code are available on my website (link below).
+
+## Docs and Guides
+Documentation, specifications, and setup guides are available on my website.
+
+[Docs and Articles](https://www.vaughnnugent.com/resources/software/articles?tags=docs,_VNLib.Plugins.Essentials.Auth.Social)
+[Builds and Source](https://www.vaughnnugent.com/resources/software/modules/Plugins.Essentials)
+[Nuget Feeds](https://www.vaughnnugent.com/resources/software/modules)
+
+## License
+All source files in this repository is licensed under the GNU Affero General Public License (or any later version).
+See the LICENSE file for more information.
diff --git a/plugins/VNLib.Plugins.Essentials.Auth.Social/build.readme.md b/plugins/VNLib.Plugins.Essentials.Auth.Social/build.readme.md
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/plugins/VNLib.Plugins.Essentials.Auth.Social/build.readme.md
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");
+ }
+ }
+}
diff --git a/plugins/VNLib.Plugins.Essentials.Content.Routing/src/VNLib.Plugins.Essentials.Content.Routing.csproj b/plugins/VNLib.Plugins.Essentials.Content.Routing/src/VNLib.Plugins.Essentials.Content.Routing.csproj
index df4c4cd..aa3fa56 100644
--- a/plugins/VNLib.Plugins.Essentials.Content.Routing/src/VNLib.Plugins.Essentials.Content.Routing.csproj
+++ b/plugins/VNLib.Plugins.Essentials.Content.Routing/src/VNLib.Plugins.Essentials.Content.Routing.csproj
@@ -56,9 +56,9 @@
<ProjectReference Include="..\..\..\..\Extensions\lib\VNLib.Plugins.Extensions.Loading.Sql\src\VNLib.Plugins.Extensions.Loading.Sql.csproj" />
<ProjectReference Include="..\..\..\..\Extensions\lib\VNLib.Plugins.Extensions.Loading\src\VNLib.Plugins.Extensions.Loading.csproj" />
</ItemGroup>
-
+
<Target Condition="'$(BuildingInsideVisualStudio)' == true" Name="PostBuild" AfterTargets="PostBuildEvent">
- <Exec Command="start xcopy &quot;$(TargetDir)&quot; &quot;..\..\..\..\..\devplugins\$(TargetName)&quot; /E /Y /R" />
+ <Exec Command="start xcopy &quot;$(TargetDir)&quot; &quot;$(SolutionDir)devplugins\$(TargetName)&quot; /E /Y /R" />
</Target>
</Project>
diff --git a/plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/README.md b/plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/README.md
new file mode 100644
index 0000000..1cacc6b
--- /dev/null
+++ b/plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/README.md
@@ -0,0 +1,15 @@
+# VNLib.Plugins.Essentials.Auth.Auth0
+*A runtime asset library that provides enterprise Auth0 OAuth authentication to your application that is using the [Auth.Social](../../VNLib.Plugins.Essentials.Auth.Social) plugin*
+
+## Builds
+Debug build w/ symbols & xml docs, release builds, NuGet packages, and individually packaged source code are available on my website (link below).
+
+## Docs and Guides
+Documentation, specifications, and setup guides are available on my website.
+
+[Docs and Articles](https://www.vaughnnugent.com/resources/software/articles?tags=docs,_VNLib.Plugins.Essentials.Auth.Auth0)
+[Builds and Source](https://www.vaughnnugent.com/resources/software/modules/Plugins.Essentials)
+[Nuget Feeds](https://www.vaughnnugent.com/resources/software/modules)
+
+## License
+Source files in for this project are licensed to you under the GNU Affero General Public License (or any later version). See the LICENSE files for more information. \ No newline at end of file
diff --git a/plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/build.readme.md b/plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/build.readme.md
new file mode 100644
index 0000000..4533b0d
--- /dev/null
+++ b/plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/build.readme.md
@@ -0,0 +1,11 @@
+VNLib.Plugins.Essentials.Auth.Auth0 Copyright © 2024 Vaughn Nugent
+Contact: Vaughn Nugent vnpublic[at]proton.me
+
+LICENSE:
+You should have recieved a copy of the GNU Affero General Public License along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+In the future there will be more information for installtion in this file, but for now go to my website
+
+https://www.vaughnnugent.com/resources/software/articles?tags=docs,_VNLib.Plugins.Essentials.Auth.Auth0
+
+Thank you! \ No newline at end of file
diff --git a/plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/src/Auth0Portal.cs b/plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/src/Auth0Portal.cs
new file mode 100644
index 0000000..0ae92f4
--- /dev/null
+++ b/plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/src/Auth0Portal.cs
@@ -0,0 +1,67 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Auth.Auth0
+* File: Auth0Portal.cs
+*
+* Auth0Portal.cs is part of VNLib.Plugins.Essentials.Auth.Auth0 which is
+* part of the larger VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Essentials.Auth.Auth0 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.Auth0 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.Plugins.Extensions.Loading;
+using VNLib.Plugins.Extensions.Loading.Routing;
+using VNLib.Plugins.Essentials.Auth.Social;
+
+using VNLib.Plugins.Essentials.Auth.Auth0.Endpoints;
+
+namespace VNLib.Plugins.Essentials.Auth.Auth0
+{
+
+ [ServiceExport]
+ [ConfigurationName(ConfigKey)]
+ public sealed class Auth0Portal : IOAuthProvider
+ {
+ internal const string ConfigKey = "auth0";
+
+ private readonly LoginEndpoint _loginEndpoint;
+ private readonly LogoutEndpoint _logoutEndpoint;
+
+ public Auth0Portal(PluginBase plugin, IConfigScope config)
+ {
+ //Init the login endpoint
+ _loginEndpoint = plugin.Route<LoginEndpoint>();
+ _logoutEndpoint = plugin.Route<LogoutEndpoint>();
+ }
+
+ ///<inheritdoc/>
+ public SocialOAuthPortal[] GetPortals()
+ {
+
+ //Return the Auth0 portal
+ return [
+ new SocialOAuthPortal(
+ ConfigKey,
+ _loginEndpoint,
+ _logoutEndpoint
+ )
+ ];
+
+ }
+ }
+}
diff --git a/plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/src/Endpoints/LoginEndpoint.cs b/plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/src/Endpoints/LoginEndpoint.cs
new file mode 100644
index 0000000..52be461
--- /dev/null
+++ b/plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/src/Endpoints/LoginEndpoint.cs
@@ -0,0 +1,191 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Auth.Auth0
+* File: LoginEndpoint.cs
+*
+* LoginEndpoint.cs is part of VNLib.Plugins.Essentials.Auth.Auth0 which is
+* part of the larger VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Essentials.Auth.Auth0 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.Auth0 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.Threading;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+
+using RestSharp;
+
+using VNLib.Hashing;
+using VNLib.Hashing.IdentityUtility;
+using VNLib.Utils.Logging;
+using VNLib.Plugins.Essentials.Accounts;
+using VNLib.Plugins.Extensions.Loading;
+using VNLib.Net.Rest.Client.Construction;
+using VNLib.Plugins.Essentials.Auth.Social;
+
+/*
+ * Provides specialized login for Auth0 identity managment system. Auth0 apis use JWT tokens
+ * and JWK signing keys. Keys are downloaded when the plugin is first loaded and cached for
+ * the lifetime of the plugin. The keys are used to verify the JWT token and extract the user
+ */
+
+namespace VNLib.Plugins.Essentials.Auth.Auth0.Endpoints
+{
+
+ [ConfigurationName(Auth0Portal.ConfigKey)]
+ internal sealed class LoginEndpoint : SocialOauthBase
+ {
+ private readonly IAsyncLazy<ReadOnlyJsonWebKey[]> Auth0VerificationJwk;
+ private readonly bool VerifyEmail;
+
+ public LoginEndpoint(PluginBase plugin, IConfigScope config) : base(plugin, config)
+ {
+ string keyUrl = config["key_url"].GetString() ?? throw new KeyNotFoundException("Missing Auth0 'key_url' from config");
+
+ //Define the key endpoint
+ SiteAdapter.DefineSingleEndpoint()
+ .WithEndpoint<GetKeyRequest>()
+ .WithUrl(keyUrl)
+ .WithMethod(Method.Get)
+ .WithHeader("Accept", "application/json")
+ .OnResponse((r, res) => res.ThrowIfError());
+
+ //Check for email verification
+ VerifyEmail = config.TryGetValue("verified_email", out JsonElement el) && el.GetBoolean();
+
+ //Get certificate on background thread
+ Auth0VerificationJwk = Task.Run(GetRsaCertificate).AsLazy();
+ }
+
+ private async Task<ReadOnlyJsonWebKey[]> GetRsaCertificate()
+ {
+ try
+ {
+ Log.Debug("Getting Auth0 signing keys");
+
+ //rent client from pool
+ RestResponse response = await SiteAdapter.ExecuteAsync(new GetKeyRequest());
+
+ //Get response as doc
+ using JsonDocument doc = JsonDocument.Parse(response.RawBytes);
+
+ //Create a new jwk from each key element in the response
+ ReadOnlyJsonWebKey[] keys = doc.RootElement.GetProperty("keys")
+ .EnumerateArray()
+ .Select(static k => new ReadOnlyJsonWebKey(k))
+ .ToArray();
+
+ Log.Debug("Found {count} Auth0 signing keys", keys.Length);
+
+ return keys;
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Failed to get Auth0 signing keys");
+ throw;
+ }
+ }
+
+ /*
+ * Auth0 uses the format "platoform|{user_id}" for the user id so it should match the
+ * external platofrm as github and discord endoints also
+ */
+
+ private static string GetUserIdFromPlatform(string userName)
+ {
+ return ManagedHash.ComputeHash(userName, HashAlg.SHA1, HashEncodingMode.Hexadecimal);
+ }
+
+
+ private static readonly Task<UserLoginData?> EmptyLoginData = Task.FromResult<UserLoginData?>(null);
+ private static readonly Task<AccountData?> EmptyUserData = Task.FromResult<AccountData?>(null);
+
+ ///<inheritdoc/>
+ protected override Task<UserLoginData?> GetLoginDataAsync(IOAuthAccessState clientAccess, CancellationToken cancellation)
+ {
+ //recover the identity token
+ using JsonWebToken jwt = JsonWebToken.Parse(clientAccess.IdToken);
+
+ //Verify the token against the first signing key
+ if (!jwt.VerifyFromJwk(Auth0VerificationJwk.Value[0]))
+ {
+ return EmptyLoginData;
+ }
+
+ using JsonDocument userData = jwt.GetPayload();
+
+ int iat = userData.RootElement.GetProperty("iat").GetInt32();
+ int exp = userData.RootElement.GetProperty("exp").GetInt32();
+
+ string userId = userData.RootElement.GetProperty("sub").GetString() ?? throw new Exception("Missing sub in jwt");
+ string audience = userData.RootElement.GetProperty("aud").GetString() ?? throw new Exception("Missing aud in jwt");
+ string issuer = userData.RootElement.GetProperty("iss").GetString() ?? throw new Exception("Missing iss in jwt");
+
+ if (exp < DateTimeOffset.UtcNow.ToUnixTimeSeconds())
+ {
+ //Expired
+ return EmptyLoginData;
+ }
+
+ //Verify audience matches client id
+ if (!Config.ClientID.Value.Equals(audience, StringComparison.Ordinal))
+ {
+ //Invalid audience
+ return EmptyLoginData;
+ }
+
+ return Task.FromResult<UserLoginData?>(new UserLoginData()
+ {
+ UserId = GetUserIdFromPlatform(userId)
+ });
+ }
+
+ /*
+ * Account data may be recovered from the identity token
+ * and it happens after a call to GetLoginData so
+ * we do not need to re-verify the token
+ */
+ ///<inheritdoc/>
+ protected override Task<AccountData?> GetAccountDataAsync(IOAuthAccessState clientAccess, CancellationToken cancellationToken)
+ {
+ //Parse token again to get the user data
+ using JsonWebToken jwt = JsonWebToken.Parse(clientAccess.IdToken);
+
+ using JsonDocument userData = jwt.GetPayload();
+
+ //Confirm email is verified
+ if (!userData.RootElement.GetProperty("email_verified").GetBoolean() && VerifyEmail)
+ {
+ return EmptyUserData;
+ }
+
+ string fullName = userData.RootElement.GetProperty("name").GetString() ?? " ";
+
+ return Task.FromResult<AccountData?>(new AccountData()
+ {
+ EmailAddress = userData.RootElement.GetProperty("email").GetString(),
+ First = fullName.Split(' ').FirstOrDefault(),
+ Last = fullName.Split(' ').LastOrDefault(),
+ });
+ }
+
+ private sealed record class GetKeyRequest()
+ { }
+ }
+}
diff --git a/plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/src/Endpoints/LogoutEndpoint.cs b/plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/src/Endpoints/LogoutEndpoint.cs
new file mode 100644
index 0000000..497357a
--- /dev/null
+++ b/plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/src/Endpoints/LogoutEndpoint.cs
@@ -0,0 +1,70 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Auth.Auth0
+* File: LogoutEndpoint.cs
+*
+* LogoutEndpoint.cs is part of VNLib.Plugins.Essentials.Auth.Auth0 which is
+* part of the larger VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Essentials.Auth.Auth0 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.Auth0 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 VNLib.Utils;
+
+using VNLib.Plugins.Extensions.Loading;
+using VNLib.Plugins.Essentials.Accounts;
+using VNLib.Plugins.Essentials.Endpoints;
+using VNLib.Plugins.Essentials.Extensions;
+
+
+namespace VNLib.Plugins.Essentials.Auth.Auth0.Endpoints
+{
+ [ConfigurationName(Auth0Portal.ConfigKey)]
+ internal sealed class LogoutEndpoint : ProtectedWebEndpoint
+ {
+ private readonly IAsyncLazy<string> ReturnUrl;
+
+ public LogoutEndpoint(PluginBase plugin, IConfigScope config)
+ {
+ string returnToUrl = config.GetRequiredProperty("return_to_url", p => p.GetString()!);
+ string logoutUrl = config.GetRequiredProperty("logout_url", p => p.GetString()!);
+ string path = config.GetRequiredProperty("path", p => p.GetString()!);
+
+ InitPathAndLog($"{path}/logout", plugin.Log);
+
+ //Build the return url once the client id is available
+ ReturnUrl = plugin.GetSecretAsync("auth0_client_id").ToLazy(sr =>
+ {
+ return $"{logoutUrl}?client_id={sr.Result.ToString()}&returnTo={returnToUrl}";
+ });
+ }
+
+ protected override ERRNO PreProccess(HttpEntity entity)
+ {
+ //Client required to be fully authorized
+ return base.PreProccess(entity)
+ && entity.IsClientAuthorized(AuthorzationCheckLevel.Critical);
+ }
+
+ protected override VfReturnType Post(HttpEntity entity)
+ {
+ //Invalidate the login before redirecting the client
+ entity.InvalidateLogin();
+ entity.Redirect(RedirectType.Temporary, ReturnUrl.Value);
+ return VfReturnType.VirtualSkip;
+ }
+ }
+} \ No newline at end of file
diff --git a/plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/src/VNLib.Plugins.Essentials.Auth.Auth0.csproj b/plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/src/VNLib.Plugins.Essentials.Auth.Auth0.csproj
new file mode 100644
index 0000000..2beb64f
--- /dev/null
+++ b/plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/src/VNLib.Plugins.Essentials.Auth.Auth0.csproj
@@ -0,0 +1,48 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Nullable>enable</Nullable>
+ <TargetFramework>net8.0</TargetFramework>
+ <RootNamespace>VNLib.Plugins.Essentials.Auth.Auth0</RootNamespace>
+ <AssemblyName>VNLib.Plugins.Essentials.Auth.Auth0</AssemblyName>
+ <GenerateDocumentationFile>True</GenerateDocumentationFile>
+ <AnalysisLevel>latest-all</AnalysisLevel>
+ <NeutralLanguage>en-US</NeutralLanguage>
+ <EnableDynamicLoading>true</EnableDynamicLoading>
+ </PropertyGroup>
+
+ <PropertyGroup>
+ <Authors>Vaughn Nugent</Authors>
+ <Company>Vaughn Nugent</Company>
+ <Product>VNLib.Plugins.Essentials.Auth.Auth0</Product>
+ <PackageId>VNLib.Plugins.Essentials.Auth.Auth0</PackageId>
+ <Description>A runtime asset library that adds Auth0 social OAuth autentication integration with Auth.Social plugin library</Description>
+ <Copyright>Copyright © 2024 Vaughn Nugent</Copyright>
+ <PackageProjectUrl>https://www.vaughnnugent.com/resources/software/modules/Plugins.Essentials</PackageProjectUrl>
+ <RepositoryUrl>https://Auth0.com/VnUgE/Plugins.Essentials/tree/master/plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0</RepositoryUrl>
+ <PackageReadmeFile>README.md</PackageReadmeFile>
+ <PackageLicenseFile>LICENSE</PackageLicenseFile>
+ <PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance>
+ </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>
+ <ProjectReference Include="..\..\..\..\plugins\VNLib.Plugins.Essentials.Auth.Social\src\VNLib.Plugins.Essentials.Auth.Social.csproj" />
+ </ItemGroup>
+
+ <Target Condition="'$(BuildingInsideVisualStudio)' == true" Name="PostBuild" AfterTargets="PostBuildEvent">
+ <Exec Command="start xcopy &quot;$(TargetDir)&quot; &quot;$(SolutionDir)devplugins\RuntimeAssets\$(TargetName)&quot; /E /Y /R" />
+ </Target>
+
+</Project>
diff --git a/plugins/providers/VNLib.Plugins.Essentials.Auth.Discord/README.md b/plugins/providers/VNLib.Plugins.Essentials.Auth.Discord/README.md
new file mode 100644
index 0000000..fd523ad
--- /dev/null
+++ b/plugins/providers/VNLib.Plugins.Essentials.Auth.Discord/README.md
@@ -0,0 +1,15 @@
+# VNLib.Plugins.Essentials.Auth.Discord
+*A runtime asset library that provides Discord OAuth authentication to your application that is using the [Auth.Social](../../VNLib.Plugins.Essentials.Auth.Social) plugin*
+
+## Builds
+Debug build w/ symbols & xml docs, release builds, NuGet packages, and individually packaged source code are available on my website (link below).
+
+## Docs and Guides
+Documentation, specifications, and setup guides are available on my website.
+
+[Docs and Articles](https://www.vaughnnugent.com/resources/software/articles?tags=docs,_VNLib.Plugins.Essentials.Auth.Discord)
+[Builds and Source](https://www.vaughnnugent.com/resources/software/modules/Plugins.Essentials)
+[Nuget Feeds](https://www.vaughnnugent.com/resources/software/modules)
+
+## License
+Source files in for this project are licensed to you under the GNU Affero General Public License (or any later version). See the LICENSE files for more information. \ No newline at end of file
diff --git a/plugins/providers/VNLib.Plugins.Essentials.Auth.Discord/build.readme.md b/plugins/providers/VNLib.Plugins.Essentials.Auth.Discord/build.readme.md
new file mode 100644
index 0000000..3098245
--- /dev/null
+++ b/plugins/providers/VNLib.Plugins.Essentials.Auth.Discord/build.readme.md
@@ -0,0 +1,11 @@
+VNLib.Plugins.Essentials.Auth.Discord Copyright © 2024 Vaughn Nugent
+Contact: Vaughn Nugent vnpublic[at]proton.me
+
+LICENSE:
+You should have recieved a copy of the GNU Affero General Public License along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+In the future there will be more information for installtion in this file, but for now go to my website
+
+https://www.vaughnnugent.com/resources/software/articles?tags=docs,_VNLib.Plugins.Essentials.Auth.Discord
+
+Thank you! \ No newline at end of file
diff --git a/plugins/providers/VNLib.Plugins.Essentials.Auth.Discord/src/DiscordPortal.cs b/plugins/providers/VNLib.Plugins.Essentials.Auth.Discord/src/DiscordPortal.cs
new file mode 100644
index 0000000..5b0503e
--- /dev/null
+++ b/plugins/providers/VNLib.Plugins.Essentials.Auth.Discord/src/DiscordPortal.cs
@@ -0,0 +1,65 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Auth.Discord
+* File: DiscordPortal.cs
+*
+* DiscordPortal.cs is part of VNLib.Plugins.Essentials.Auth.Discord which is
+* part of the larger VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Essentials.Auth.Discord 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.Discord 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.Plugins.Extensions.Loading;
+using VNLib.Plugins.Extensions.Loading.Routing;
+using VNLib.Plugins.Essentials.Auth.Social;
+
+using VNLib.Plugins.Essentials.Auth.Discord.Endpoints;
+
+namespace VNLib.Plugins.Essentials.Auth.Discord
+{
+
+ [ServiceExport]
+ [ConfigurationName(ConfigKey)]
+ public sealed class DiscordPortal : IOAuthProvider
+ {
+ internal const string ConfigKey = "discord";
+
+ private readonly DiscordOauth _loginEndpoint;
+
+ public DiscordPortal(PluginBase plugin, IConfigScope config)
+ {
+ //Init the login endpoint
+ _loginEndpoint = plugin.Route<DiscordOauth>();
+ }
+
+ ///<inheritdoc/>
+ public SocialOAuthPortal[] GetPortals()
+ {
+
+ //Return the Discord portal
+ return [
+ new SocialOAuthPortal(
+ ConfigKey,
+ _loginEndpoint,
+ null
+ )
+ ];
+
+ }
+ }
+}
diff --git a/plugins/providers/VNLib.Plugins.Essentials.Auth.Discord/src/Endpoint/DiscordOauth.cs b/plugins/providers/VNLib.Plugins.Essentials.Auth.Discord/src/Endpoint/DiscordOauth.cs
new file mode 100644
index 0000000..4aa7a64
--- /dev/null
+++ b/plugins/providers/VNLib.Plugins.Essentials.Auth.Discord/src/Endpoint/DiscordOauth.cs
@@ -0,0 +1,148 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Auth.Discord
+* File: DiscordOauth.cs
+*
+* DiscordOauth.cs is part of VNLib.Plugins.Essentials.Auth.Discord which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Essentials.Auth.Discord 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.Discord 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;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Text.Json.Serialization;
+
+using RestSharp;
+
+using VNLib.Hashing;
+using VNLib.Utils.Logging;
+using VNLib.Plugins.Essentials.Accounts;
+using VNLib.Plugins.Extensions.Loading;
+using VNLib.Net.Rest.Client.Construction;
+using VNLib.Plugins.Essentials.Auth.Social;
+
+namespace VNLib.Plugins.Essentials.Auth.Discord.Endpoints
+{
+ [ConfigurationName(DiscordPortal.ConfigKey)]
+ internal sealed class DiscordOauth : SocialOauthBase
+ {
+ public DiscordOauth(PluginBase plugin, IConfigScope config) : base(plugin, config)
+ {
+ //Define profile endpoint
+ SiteAdapter.DefineSingleEndpoint()
+ .WithEndpoint<DiscordProfileRequest>()
+ .WithMethod(Method.Get)
+ .WithUrl(Config.UserDataUrl)
+ .WithHeader("Authorization", r => $"{r.AccessToken.Type} {r.AccessToken.Token}");
+ }
+
+ /*
+ * Creates a user-id from the users discord username, that is repeatable
+ * and matches the Auth0 social user-id format
+ */
+ private static string GetUserIdFromPlatform(string userName) => $"discord|{userName}";
+
+
+ ///<inheritdoc/>
+ protected override async Task<AccountData?> GetAccountDataAsync(IOAuthAccessState accessToken, CancellationToken cancellationToken)
+ {
+ //Get the user's profile
+ UserProfile? profile = await GetUserProfileAssync(accessToken, cancellationToken);
+
+ if (profile == null)
+ {
+ return null;
+ }
+
+ //Make sure the user's account is verified
+ if (!profile.Verified)
+ {
+ return null;
+ }
+
+ return new()
+ {
+ EmailAddress = profile.EmailAddress,
+ First = profile.Username,
+ };
+ }
+
+ ///<inheritdoc/>
+ protected override async Task<UserLoginData?> GetLoginDataAsync(IOAuthAccessState accessToken, CancellationToken cancellationToken)
+ {
+ //Get the user's profile
+ UserProfile? profile = await GetUserProfileAssync(accessToken, cancellationToken);
+
+ if (profile == null)
+ {
+ return null;
+ }
+
+ return new()
+ {
+ //Get unique user-id from the discord profile and sha1 hex hash to store in db
+ UserId = GetUserIdFromPlatform(profile.UserID)
+ };
+ }
+
+ private async Task<UserProfile?> GetUserProfileAssync(IOAuthAccessState accessToken, CancellationToken cancellationToken)
+ {
+ //Get the user's email address's
+ DiscordProfileRequest req = new(accessToken);
+ RestResponse response = await SiteAdapter.ExecuteAsync(req, cancellationToken);
+
+ //Check response
+ if (!response.IsSuccessful || response.Content == null)
+ {
+ Log.Debug("Discord user request responded with code {code}:{data}", response.StatusCode, response.Content);
+ return null;
+ }
+
+ UserProfile? discordProfile = JsonSerializer.Deserialize<UserProfile>(response.RawBytes);
+
+ if (string.IsNullOrWhiteSpace(discordProfile?.UserID))
+ {
+ Log.Debug("Discord user request responded with invalid response data {code}:{data}", response.StatusCode, response.Content);
+ return null;
+ }
+
+ return discordProfile;
+ }
+
+ private sealed record class DiscordProfileRequest(IOAuthAccessState AccessToken)
+ { }
+
+ /*
+ * Matches the profile endpoint (@me) json object
+ */
+ private sealed class UserProfile
+ {
+ [JsonPropertyName("username")]
+ public string? Username { get; set; }
+ [JsonPropertyName("id")]
+ public string? UserID { get; set; }
+ [JsonPropertyName("url")]
+ public string? ProfileUrl { get; set; }
+ [JsonPropertyName("verified")]
+ public bool Verified { get; set; }
+ [JsonPropertyName("email")]
+ public string? EmailAddress { get; set; }
+ }
+ }
+} \ No newline at end of file
diff --git a/plugins/providers/VNLib.Plugins.Essentials.Auth.Discord/src/VNLib.Plugins.Essentials.Auth.Discord.csproj b/plugins/providers/VNLib.Plugins.Essentials.Auth.Discord/src/VNLib.Plugins.Essentials.Auth.Discord.csproj
new file mode 100644
index 0000000..d64ebe6
--- /dev/null
+++ b/plugins/providers/VNLib.Plugins.Essentials.Auth.Discord/src/VNLib.Plugins.Essentials.Auth.Discord.csproj
@@ -0,0 +1,48 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Nullable>enable</Nullable>
+ <TargetFramework>net8.0</TargetFramework>
+ <RootNamespace>VNLib.Plugins.Essentials.Auth.Discord</RootNamespace>
+ <AssemblyName>VNLib.Plugins.Essentials.Auth.Discord</AssemblyName>
+ <GenerateDocumentationFile>True</GenerateDocumentationFile>
+ <AnalysisLevel>latest-all</AnalysisLevel>
+ <NeutralLanguage>en-US</NeutralLanguage>
+ <EnableDynamicLoading>true</EnableDynamicLoading>
+ </PropertyGroup>
+
+ <PropertyGroup>
+ <Authors>Vaughn Nugent</Authors>
+ <Company>Vaughn Nugent</Company>
+ <Product>VNLib.Plugins.Essentials.Auth.Discord</Product>
+ <PackageId>VNLib.Plugins.Essentials.Auth.Discord</PackageId>
+ <Description>A runtime asset library that adds Discord social OAuth autentication integration with Auth.Social plugin library</Description>
+ <Copyright>Copyright © 2024 Vaughn Nugent</Copyright>
+ <PackageProjectUrl>https://www.vaughnnugent.com/resources/software/modules/Plugins.Essentials</PackageProjectUrl>
+ <RepositoryUrl>https://Discord.com/VnUgE/Plugins.Essentials/tree/master/plugins/providers/VNLib.Plugins.Essentials.Auth.Discord</RepositoryUrl>
+ <PackageReadmeFile>README.md</PackageReadmeFile>
+ <PackageLicenseFile>LICENSE</PackageLicenseFile>
+ <PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance>
+ </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>
+ <ProjectReference Include="..\..\..\..\plugins\VNLib.Plugins.Essentials.Auth.Social\src\VNLib.Plugins.Essentials.Auth.Social.csproj" />
+ </ItemGroup>
+
+ <Target Condition="'$(BuildingInsideVisualStudio)' == true" Name="PostBuild" AfterTargets="PostBuildEvent">
+ <Exec Command="start xcopy &quot;$(TargetDir)&quot; &quot;$(SolutionDir)devplugins\RuntimeAssets\$(TargetName)&quot; /E /Y /R" />
+ </Target>
+
+</Project>
diff --git a/plugins/providers/VNLib.Plugins.Essentials.Auth.Github/README.md b/plugins/providers/VNLib.Plugins.Essentials.Auth.Github/README.md
new file mode 100644
index 0000000..c4c91dd
--- /dev/null
+++ b/plugins/providers/VNLib.Plugins.Essentials.Auth.Github/README.md
@@ -0,0 +1,15 @@
+# VNLib.Plugins.Essentials.Auth.Github
+*A runtime asset library that provides Github OAuth authentication to your application that is using the [Auth.Social](../../VNLib.Plugins.Essentials.Auth.Social) plugin*
+
+## Builds
+Debug build w/ symbols & xml docs, release builds, NuGet packages, and individually packaged source code are available on my website (link below).
+
+## Docs and Guides
+Documentation, specifications, and setup guides are available on my website.
+
+[Docs and Articles](https://www.vaughnnugent.com/resources/software/articles?tags=docs,_VNLib.Plugins.Essentials.Auth.Github)
+[Builds and Source](https://www.vaughnnugent.com/resources/software/modules/Plugins.Essentials)
+[Nuget Feeds](https://www.vaughnnugent.com/resources/software/modules)
+
+## License
+Source files in for this project are licensed to you under the GNU Affero General Public License (or any later version). See the LICENSE files for more information. \ No newline at end of file
diff --git a/plugins/providers/VNLib.Plugins.Essentials.Auth.Github/build.readme.md b/plugins/providers/VNLib.Plugins.Essentials.Auth.Github/build.readme.md
new file mode 100644
index 0000000..3b7c356
--- /dev/null
+++ b/plugins/providers/VNLib.Plugins.Essentials.Auth.Github/build.readme.md
@@ -0,0 +1,11 @@
+VNLib.Plugins.Essentials.Auth.Github Copyright © 2024 Vaughn Nugent
+Contact: Vaughn Nugent vnpublic[at]proton.me
+
+LICENSE:
+You should have recieved a copy of the GNU Affero General Public License along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+In the future there will be more information for installtion in this file, but for now go to my website
+
+https://www.vaughnnugent.com/resources/software/articles?tags=docs,_VNLib.Plugins.Essentials.Auth.Github
+
+Thank you! \ No newline at end of file
diff --git a/plugins/providers/VNLib.Plugins.Essentials.Auth.Github/src/Endpoint/GitHubOauth.cs b/plugins/providers/VNLib.Plugins.Essentials.Auth.Github/src/Endpoint/GitHubOauth.cs
new file mode 100644
index 0000000..b23188c
--- /dev/null
+++ b/plugins/providers/VNLib.Plugins.Essentials.Auth.Github/src/Endpoint/GitHubOauth.cs
@@ -0,0 +1,213 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Auth.Github
+* File: GitHubOauth.cs
+*
+* GitHubOauth.cs is part ofVNLib.Plugins.Essentials.Auth.Githubwhich is part of the larger
+* VNLib collection of libraries and utilities.
+*
+*VNLib.Plugins.Essentials.Auth.Githubis 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.Githubis 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.Threading;
+using System.Text.Json;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+using RestSharp;
+
+using VNLib.Utils.Logging;
+using VNLib.Plugins.Essentials.Accounts;
+using VNLib.Plugins.Extensions.Loading;
+using VNLib.Net.Rest.Client.Construction;
+using VNLib.Plugins.Essentials.Auth.Social;
+
+namespace VNLib.Plugins.Essentials.Auth.Github.Endpoints
+{
+ [ConfigurationName(GithubPortal.ConfigKey)]
+ internal sealed partial class GitHubOauth : SocialOauthBase
+ {
+ private const string GITHUB_V3_ACCEPT = "application/vnd.github.v3+json";
+
+ private readonly string UserEmailUrl;
+
+
+ public GitHubOauth(PluginBase plugin, IConfigScope config) : base(plugin, config)
+ {
+ UserEmailUrl = config["user_email_url"].GetString() ?? throw new KeyNotFoundException("Missing required key 'user_email_url' for github configuration");
+
+ //Define profile endpoint, gets users required profile information
+ SiteAdapter.DefineSingleEndpoint()
+ .WithEndpoint<GetProfileRequest>()
+ .WithMethod(Method.Get)
+ .WithUrl(Config.UserDataUrl)
+ .WithHeader("Accept", GITHUB_V3_ACCEPT)
+ .WithHeader("Authorization", at => $"{at.AccessToken.Type} {at.AccessToken.Token}");
+
+ //Define email endpoint, gets users email address
+ SiteAdapter.DefineSingleEndpoint()
+ .WithEndpoint<GetEmailRequest>()
+ .WithMethod(Method.Get)
+ .WithUrl(UserEmailUrl)
+ .WithHeader("Accept", GITHUB_V3_ACCEPT)
+ .WithHeader("Authorization", at => $"{at.AccessToken.Type} {at.AccessToken.Token}");
+ }
+
+ /*
+ * Creates a repeatable, and source specific user id for
+ * GitHub users. This format is identical to the algorithim used
+ * in the Auth0 Github connection, so it is compatible with Auth0
+ */
+ private static string GetUserIdFromPlatform(int userId) => $"github|{userId}";
+
+
+ protected override async Task<UserLoginData?> GetLoginDataAsync(IOAuthAccessState accessToken, CancellationToken cancellationToken)
+ {
+ GetProfileRequest req = new(accessToken);
+
+ //Exec the get for the profile
+ RestResponse profResponse = await SiteAdapter.ExecuteAsync(req, cancellationToken);
+
+ if (!profResponse.IsSuccessful || profResponse.RawBytes == null)
+ {
+ Log.Debug("Github login data attempt responded with status code {code}", profResponse.StatusCode);
+ return null;
+ }
+
+ GithubProfile profile = JsonSerializer.Deserialize<GithubProfile>(profResponse.RawBytes)!;
+
+ if (profile.ID < 100)
+ {
+ Log.Debug("Github login data attempt responded with empty or invalid response body", profResponse.StatusCode);
+ return null;
+ }
+
+ //Return login data
+ return new()
+ {
+ //User-id is just the SHA 1
+ UserId = GetUserIdFromPlatform(profile.ID)
+ };
+ }
+
+ protected override async Task<AccountData?> GetAccountDataAsync(IOAuthAccessState accessToken, CancellationToken cancellationToken = default)
+ {
+ AccountData? accountData = null;
+
+ //Get the user's email address's
+ GetEmailRequest request = new(accessToken);
+
+ //get user's emails
+ RestResponse getEmailResponse = await SiteAdapter.ExecuteAsync(request, cancellationToken);
+
+ //Check status
+ if (getEmailResponse.IsSuccessful && getEmailResponse.RawBytes != null)
+ {
+ //Filter emails addresses
+ foreach (EmailContainer email in JsonSerializer.Deserialize<EmailContainer[]>(getEmailResponse.RawBytes)!)
+ {
+ //Capture the first primary email address and make sure its verified
+ if (email.Primary && email.Verified)
+ {
+ accountData = new()
+ {
+ //store email on current profile
+ EmailAddress = email.Email
+ };
+ goto Continue;
+ }
+ }
+ //No primary email found
+ return null;
+ }
+ else
+ {
+ Log.Debug("Github account data request failed but GH responded with status code {code}", getEmailResponse.StatusCode);
+ return null;
+ }
+ Continue:
+
+ //We need to get the user's profile again
+ GetProfileRequest prof = new(accessToken);
+
+ //Exec request against site adapter
+ RestResponse profResponse = await SiteAdapter.ExecuteAsync(prof, cancellationToken);
+
+ if (!profResponse.IsSuccessful || profResponse.RawBytes == null)
+ {
+ Log.Debug("Github account data request failed but GH responded with status code {code}", profResponse.StatusCode);
+ return null;
+ }
+
+ //Deserialize the profile
+ GithubProfile profile = JsonSerializer.Deserialize<GithubProfile>(profResponse.RawBytes)!;
+
+ //Get the user's name from gh profile
+ string[] names = profile.FullName!.Split(" ", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+
+ //setup the user's profile data
+ accountData.First = names.Length > 0 ? names[0] : string.Empty;
+ accountData.Last = names.Length > 1 ? names[1] : string.Empty;
+ return accountData;
+ }
+
+ //Requests to get required data from github
+
+ private sealed record class GetProfileRequest(IOAuthAccessState AccessToken)
+ { }
+
+ private sealed record class GetEmailRequest(IOAuthAccessState AccessToken)
+ { }
+
+ /*
+ * Matches the json result from the
+ */
+ private sealed class GithubProfile
+ {
+ [JsonPropertyName("login")]
+ public string? Username { get; set; }
+ [JsonPropertyName("id")]
+ public int ID { get; set; }
+ [JsonPropertyName("node_id")]
+ public string? NodeID { get; set; }
+ [JsonPropertyName("avatar_url")]
+ public string? AvatarUrl { get; set; }
+ [JsonPropertyName("url")]
+ public string? ProfileUrl { get; set; }
+ [JsonPropertyName("type")]
+ public string? Type { get; set; }
+ [JsonPropertyName("name")]
+ public string? FullName { get; set; }
+ [JsonPropertyName("company")]
+ public string? Company { get; set; }
+ }
+ /*
+ * Matches the required data from the github email endpoint
+ */
+ private sealed class EmailContainer
+ {
+ [JsonPropertyName("email")]
+ public string? Email { get; set; }
+ [JsonPropertyName("primary")]
+ public bool Primary { get; set; }
+ [JsonPropertyName("verified")]
+ public bool Verified { get; set; }
+ }
+
+ }
+} \ No newline at end of file
diff --git a/plugins/providers/VNLib.Plugins.Essentials.Auth.Github/src/GithubPortal.cs b/plugins/providers/VNLib.Plugins.Essentials.Auth.Github/src/GithubPortal.cs
new file mode 100644
index 0000000..99b0ebf
--- /dev/null
+++ b/plugins/providers/VNLib.Plugins.Essentials.Auth.Github/src/GithubPortal.cs
@@ -0,0 +1,65 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Auth.Github
+* File: GithubPortal.cs
+*
+* GithubPortal.cs is part ofVNLib.Plugins.Essentials.Auth.Githubwhich is
+* part of the larger VNLib collection of libraries and utilities.
+*
+*VNLib.Plugins.Essentials.Auth.Githubis 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.Githubis 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.Plugins.Extensions.Loading;
+using VNLib.Plugins.Extensions.Loading.Routing;
+using VNLib.Plugins.Essentials.Auth.Social;
+
+using VNLib.Plugins.Essentials.Auth.Github.Endpoints;
+
+namespace VNLib.Plugins.Essentials.Auth.Github
+{
+
+ [ServiceExport]
+ [ConfigurationName(ConfigKey)]
+ public sealed class GithubPortal : IOAuthProvider
+ {
+ internal const string ConfigKey = "github";
+
+ private readonly GitHubOauth _loginEndpoint;
+
+ public GithubPortal(PluginBase plugin, IConfigScope config)
+ {
+ //Init the login endpoint
+ _loginEndpoint = plugin.Route<GitHubOauth>();
+ }
+
+ ///<inheritdoc/>
+ public SocialOAuthPortal[] GetPortals()
+ {
+
+ //Return the github portal
+ return [
+ new SocialOAuthPortal(
+ ConfigKey,
+ _loginEndpoint,
+ null
+ )
+ ];
+
+ }
+ }
+}
diff --git a/plugins/providers/VNLib.Plugins.Essentials.Auth.Github/src/VNLib.Plugins.Essentials.Auth.Github.csproj b/plugins/providers/VNLib.Plugins.Essentials.Auth.Github/src/VNLib.Plugins.Essentials.Auth.Github.csproj
new file mode 100644
index 0000000..e6643f9
--- /dev/null
+++ b/plugins/providers/VNLib.Plugins.Essentials.Auth.Github/src/VNLib.Plugins.Essentials.Auth.Github.csproj
@@ -0,0 +1,48 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Nullable>enable</Nullable>
+ <TargetFramework>net8.0</TargetFramework>
+ <RootNamespace>VNLib.Plugins.Essentials.Auth.Github</RootNamespace>
+ <AssemblyName>VNLib.Plugins.Essentials.Auth.Github</AssemblyName>
+ <GenerateDocumentationFile>True</GenerateDocumentationFile>
+ <AnalysisLevel>latest-all</AnalysisLevel>
+ <NeutralLanguage>en-US</NeutralLanguage>
+ <EnableDynamicLoading>true</EnableDynamicLoading>
+ </PropertyGroup>
+
+ <PropertyGroup>
+ <Authors>Vaughn Nugent</Authors>
+ <Company>Vaughn Nugent</Company>
+ <Product>VNLib.Plugins.Essentials.Auth.Github</Product>
+ <PackageId>VNLib.Plugins.Essentials.Auth.Github</PackageId>
+ <Description>A runtime asset library that adds Github social OAuth autentication integration with Auth.Social plugin library</Description>
+ <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/providers/VNLib.Plugins.Essentials.Auth.Github</RepositoryUrl>
+ <PackageReadmeFile>README.md</PackageReadmeFile>
+ <PackageLicenseFile>LICENSE</PackageLicenseFile>
+ <PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance>
+ </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>
+ <ProjectReference Include="..\..\..\..\plugins\VNLib.Plugins.Essentials.Auth.Social\src\VNLib.Plugins.Essentials.Auth.Social.csproj" />
+ </ItemGroup>
+
+ <Target Condition="'$(BuildingInsideVisualStudio)' == true" Name="PostBuild" AfterTargets="PostBuildEvent">
+ <Exec Command="start xcopy &quot;$(TargetDir)&quot; &quot;$(SolutionDir)devplugins\RuntimeAssets\$(TargetName)&quot; /E /Y /R" />
+ </Target>
+
+</Project>