diff options
author | vnugent <public@vaughnnugent.com> | 2024-01-06 18:06:01 -0500 |
---|---|---|
committer | vnugent <public@vaughnnugent.com> | 2024-01-06 18:06:01 -0500 |
commit | 3bd7effc15d0b87adce01281b073aa1db67d3cba (patch) | |
tree | e8fcf15b9d6664bcd48bb17ac2c71c70abda204d /plugins/providers/VNLib.Plugins.Essentials.Auth.Github/src | |
parent | f4c2c9e148374f462592c19e8ffd4db14672805d (diff) |
social portal conversion, pull provider libraries & include some prebuilts
Diffstat (limited to 'plugins/providers/VNLib.Plugins.Essentials.Auth.Github/src')
3 files changed, 326 insertions, 0 deletions
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> |