From cd5c949b6f43c65f94f4d7bf6eb535ce6996739e Mon Sep 17 00:00:00 2001 From: vnugent Date: Mon, 7 Aug 2023 20:10:37 -0400 Subject: Essentials, and build taskfile updates --- .../src/Endpoints/GitHubOauth.cs | 163 ++++++++++++--------- 1 file changed, 96 insertions(+), 67 deletions(-) (limited to 'plugins/VNLib.Plugins.Essentials.SocialOauth/src/Endpoints/GitHubOauth.cs') diff --git a/plugins/VNLib.Plugins.Essentials.SocialOauth/src/Endpoints/GitHubOauth.cs b/plugins/VNLib.Plugins.Essentials.SocialOauth/src/Endpoints/GitHubOauth.cs index 8446b0f..1fd691b 100644 --- a/plugins/VNLib.Plugins.Essentials.SocialOauth/src/Endpoints/GitHubOauth.cs +++ b/plugins/VNLib.Plugins.Essentials.SocialOauth/src/Endpoints/GitHubOauth.cs @@ -24,6 +24,7 @@ using System; using System.Threading; +using System.Text.Json; using System.Threading.Tasks; using System.Collections.Generic; using System.Text.Json.Serialization; @@ -32,9 +33,10 @@ using RestSharp; using VNLib.Hashing; using VNLib.Utils.Logging; -using VNLib.Net.Rest.Client; using VNLib.Plugins.Essentials.Accounts; using VNLib.Plugins.Extensions.Loading; +using VNLib.Net.Rest.Client.Construction; + namespace VNLib.Plugins.Essentials.SocialOauth.Endpoints { @@ -49,99 +51,79 @@ namespace VNLib.Plugins.Essentials.SocialOauth.Endpoints 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"); - } - - protected override void StaticClientPoolInitializer(RestClient client) - { - //add accept types of normal json and github json - client.AcceptedContentTypes = new string[2] { "application/json", GITHUB_V3_ACCEPT }; - } - /* - * 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; } + //Define profile endpoint, gets users required profile information + SiteAdapter.DefineSingleEndpoint() + .WithEndpoint() + .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() + .WithMethod(Method.Get) + .WithUrl(UserEmailUrl) + .WithHeader("Authorization", at => $"{at.AccessToken.Type} {at.AccessToken.Token}") + .WithHeader("Accept", GITHUB_V3_ACCEPT); } + /* - * Matches the required data from the github email endpoint + * 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 sealed class EmailContainer - { - [JsonPropertyName("email")] - public string? Email { get; set; } - [JsonPropertyName("primary")] - public bool Primary { get; set; } - [JsonPropertyName("verified")] - public bool Verified { get; set; } - } - private static string GetUserIdFromPlatform(int userId) { return ManagedHash.ComputeHash($"github|{userId}", HashAlg.SHA1, HashEncodingMode.Hexadecimal); } + protected override async Task GetLoginDataAsync(IOAuthAccessState accessToken, CancellationToken cancellationToken) { - //Get the user's email address's - RestRequest request = new(Config.UserDataUrl, Method.Get); - - //Add authorization token - request.AddHeader("Authorization", $"{accessToken.Type} {accessToken.Token}"); - - //Get new client from pool - using ClientContract client = ClientPool.Lease(); + GetProfileRequest req = new(accessToken); //Exec the get for the profile - RestResponse profResponse = await client.Resource.ExecuteAsync(request, cancellationToken); + RestResponse profResponse = await SiteAdapter.ExecuteAsync(req, cancellationToken); - if (!profResponse.IsSuccessful || profResponse.Data == null || profResponse.Data.ID < 100) + 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(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(profResponse.Data.ID) + UserId = GetUserIdFromPlatform(profile.ID) }; } protected override async Task GetAccountDataAsync(IOAuthAccessState accessToken, CancellationToken cancellationToken = default) { AccountData? accountData = null; - //Get the user's email address's - RestRequest request = new(UserEmailUrl, Method.Get); - //Add authorization token - request.AddHeader("Authorization", $"{accessToken.Type} {accessToken.Token}"); - using ClientContract client = ClientPool.Lease(); + //Get the user's email address's + GetEmailRequest request = new(accessToken); //get user's emails - RestResponse getEmailResponse = await client.Resource.ExecuteAsync(request, cancellationToken: cancellationToken); + RestResponse getEmailResponse = await SiteAdapter.ExecuteAsync(request, cancellationToken); + //Check status - if (getEmailResponse.IsSuccessful && getEmailResponse.Data != null) + if (getEmailResponse.IsSuccessful && getEmailResponse.RawBytes != null) { //Filter emails addresses - foreach (EmailContainer email in getEmailResponse.Data) + foreach (EmailContainer email in JsonSerializer.Deserialize(getEmailResponse.RawBytes)!) { //Capture the first primary email address and make sure its verified if (email.Primary && email.Verified) @@ -163,20 +145,24 @@ namespace VNLib.Plugins.Essentials.SocialOauth.Endpoints return null; } Continue: - //We need to get the user's profile in order to create a new account - request = new(Config.UserDataUrl, Method.Get); - //Add authorization token - request.AddHeader("Authorization", $"{accessToken.Type} {accessToken.Token}"); - //Exec the get for the profile - RestResponse profResponse = await client.Resource.ExecuteAsync(request, cancellationToken); - if (!profResponse.IsSuccessful || profResponse.Data == null) + + //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(profResponse.RawBytes)!; + //Get the user's name from gh profile - string[] names = profResponse.Data.FullName!.Split(" ", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + string[] names = profile.FullName!.Split(" ", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); //setup the user's profile data accountData.First = names.Length > 0 ? names[0] : string.Empty; @@ -184,5 +170,48 @@ namespace VNLib.Plugins.Essentials.SocialOauth.Endpoints 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 -- cgit