diff options
Diffstat (limited to 'plugins/providers')
16 files changed, 1041 insertions, 0 deletions
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 "$(TargetDir)" "$(SolutionDir)devplugins\RuntimeAssets\$(TargetName)" /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 "$(TargetDir)" "$(SolutionDir)devplugins\RuntimeAssets\$(TargetName)" /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 "$(TargetDir)" "$(SolutionDir)devplugins\RuntimeAssets\$(TargetName)" /E /Y /R" /> + </Target> + +</Project> |