From f4c2c9e148374f462592c19e8ffd4db14672805d Mon Sep 17 00:00:00 2001 From: vnugent Date: Sat, 6 Jan 2024 13:09:27 -0500 Subject: initial migration to .net 8.0 --- .../src/ClientAccessTokenState.cs | 48 -- .../src/ClientClaimManager.cs | 126 ----- .../src/ClientRequestState.cs | 81 --- .../src/Endpoints/DiscordOauth.cs | 148 ------ .../src/Endpoints/GitHubOauth.cs | 214 -------- .../src/GetTokenRequest.cs | 34 -- .../src/IOAuthAccessState.cs | 57 -- .../src/LoginClaim.cs | 73 --- .../src/LoginUriBuilder.cs | 127 ----- .../src/OAuthSiteAdapter.cs | 72 --- .../src/OauthClientConfig.cs | 148 ------ .../src/SocialEntryPoint.cs | 67 --- .../src/SocialOauthBase.cs | 572 --------------------- .../src/UserLoginData.cs | 34 -- .../VNLib.Plugins.Essentials.SocialOauth.csproj | 66 --- .../src/Validators/AccountDataValidator.cs | 83 --- .../src/Validators/LoginMessageValidation.cs | 65 --- 17 files changed, 2015 deletions(-) delete mode 100644 plugins/VNLib.Plugins.Essentials.SocialOauth/src/ClientAccessTokenState.cs delete mode 100644 plugins/VNLib.Plugins.Essentials.SocialOauth/src/ClientClaimManager.cs delete mode 100644 plugins/VNLib.Plugins.Essentials.SocialOauth/src/ClientRequestState.cs delete mode 100644 plugins/VNLib.Plugins.Essentials.SocialOauth/src/Endpoints/DiscordOauth.cs delete mode 100644 plugins/VNLib.Plugins.Essentials.SocialOauth/src/Endpoints/GitHubOauth.cs delete mode 100644 plugins/VNLib.Plugins.Essentials.SocialOauth/src/GetTokenRequest.cs delete mode 100644 plugins/VNLib.Plugins.Essentials.SocialOauth/src/IOAuthAccessState.cs delete mode 100644 plugins/VNLib.Plugins.Essentials.SocialOauth/src/LoginClaim.cs delete mode 100644 plugins/VNLib.Plugins.Essentials.SocialOauth/src/LoginUriBuilder.cs delete mode 100644 plugins/VNLib.Plugins.Essentials.SocialOauth/src/OAuthSiteAdapter.cs delete mode 100644 plugins/VNLib.Plugins.Essentials.SocialOauth/src/OauthClientConfig.cs delete mode 100644 plugins/VNLib.Plugins.Essentials.SocialOauth/src/SocialEntryPoint.cs delete mode 100644 plugins/VNLib.Plugins.Essentials.SocialOauth/src/SocialOauthBase.cs delete mode 100644 plugins/VNLib.Plugins.Essentials.SocialOauth/src/UserLoginData.cs delete mode 100644 plugins/VNLib.Plugins.Essentials.SocialOauth/src/VNLib.Plugins.Essentials.SocialOauth.csproj delete mode 100644 plugins/VNLib.Plugins.Essentials.SocialOauth/src/Validators/AccountDataValidator.cs delete mode 100644 plugins/VNLib.Plugins.Essentials.SocialOauth/src/Validators/LoginMessageValidation.cs (limited to 'plugins/VNLib.Plugins.Essentials.SocialOauth/src') diff --git a/plugins/VNLib.Plugins.Essentials.SocialOauth/src/ClientAccessTokenState.cs b/plugins/VNLib.Plugins.Essentials.SocialOauth/src/ClientAccessTokenState.cs deleted file mode 100644 index 6a86cef..0000000 --- a/plugins/VNLib.Plugins.Essentials.SocialOauth/src/ClientAccessTokenState.cs +++ /dev/null @@ -1,48 +0,0 @@ -/* -* Copyright (c) 2023 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Essentials.SocialOauth -* File: OAuthAccessState.cs -* -* OAuthAccessState.cs is part of VNLib.Plugins.Essentials.SocialOauth which -* is part of the larger VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Essentials.SocialOauth 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.SocialOauth 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.SocialOauth -{ - public class OAuthAccessState : IOAuthAccessState - { - /// - [JsonPropertyName("access_token")] - public string? Token { get; set; } - /// - [JsonPropertyName("scope")] - public string? Scope { get; set; } - /// - [JsonPropertyName("token_type")] - public string? Type { get; set; } - /// - [JsonPropertyName("refresh_token")] - public string? RefreshToken { get; set; } - /// - [JsonPropertyName("id_token")] - public string? IdToken { get; set; } - } -} \ No newline at end of file diff --git a/plugins/VNLib.Plugins.Essentials.SocialOauth/src/ClientClaimManager.cs b/plugins/VNLib.Plugins.Essentials.SocialOauth/src/ClientClaimManager.cs deleted file mode 100644 index 1e5a82e..0000000 --- a/plugins/VNLib.Plugins.Essentials.SocialOauth/src/ClientClaimManager.cs +++ /dev/null @@ -1,126 +0,0 @@ -/* -* Copyright (c) 2023 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Essentials.SocialOauth -* File: ClientClaimManager.cs -* -* ClientClaimManager.cs is part of VNLib.Plugins.Essentials.SocialOauth which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Essentials.SocialOauth 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.SocialOauth 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.SocialOauth -{ - 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 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(); - - //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.SocialOauth/src/ClientRequestState.cs b/plugins/VNLib.Plugins.Essentials.SocialOauth/src/ClientRequestState.cs deleted file mode 100644 index ba369c2..0000000 --- a/plugins/VNLib.Plugins.Essentials.SocialOauth/src/ClientRequestState.cs +++ /dev/null @@ -1,81 +0,0 @@ -/* -* Copyright (c) 2022 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Essentials.SocialOauth -* File: ClientRequestState.cs -* -* ClientRequestState.cs is part of VNLib.Plugins.Essentials.SocialOauth which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Essentials.SocialOauth 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.SocialOauth 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.SocialOauth -{ - internal sealed class ClientRequestState : ICacheable - { - private readonly ReadOnlyMemory _rawKey; - - /// - /// The raw nonce state bytes - /// - public ReadOnlyMemory State { get; private set; } - - public ClientRequestState(ReadOnlySpan keyChar, int nonceBytes) - { - //Get browser id - _rawKey = Convert.FromHexString(keyChar); - RecomputeState(nonceBytes); - } - - /// - /// Recomputes a nonce state and signature for the current - /// connection - /// - /// The size of the nonce (in bytes) to generate - public void RecomputeState(int nonceBytes) - { - //Get random nonce buffer - State = RandomHash.GetRandomBytes(nonceBytes); - } - /// - /// Computes the signature of the supplied data based on the original - /// client state for this connection - /// - /// - /// - public ERRNO ComputeSignatureForClient(ReadOnlySpan data, Span output) - { - return HMACSHA512.TryHashData(_rawKey.Span, data, output, out int count) ? count : ERRNO.E_FAIL; - } - - public DateTime Expires { get; set; } - bool IEquatable.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.SocialOauth/src/Endpoints/DiscordOauth.cs b/plugins/VNLib.Plugins.Essentials.SocialOauth/src/Endpoints/DiscordOauth.cs deleted file mode 100644 index f64d1c4..0000000 --- a/plugins/VNLib.Plugins.Essentials.SocialOauth/src/Endpoints/DiscordOauth.cs +++ /dev/null @@ -1,148 +0,0 @@ -/* -* Copyright (c) 2023 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Essentials.SocialOauth -* File: DiscordOauth.cs -* -* DiscordOauth.cs is part of VNLib.Plugins.Essentials.SocialOauth which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Essentials.SocialOauth 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.SocialOauth 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; - - -namespace VNLib.Plugins.Essentials.SocialOauth.Endpoints -{ - [ConfigurationName("discord")] - internal sealed class DiscordOauth : SocialOauthBase - { - public DiscordOauth(PluginBase plugin, IConfigScope config) : base(plugin, config) - { - //Define profile endpoint - SiteAdapter.DefineSingleEndpoint() - .WithEndpoint() - .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}"; - - - /// - protected override async Task 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, - }; - } - - /// - protected override async Task 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 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(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/VNLib.Plugins.Essentials.SocialOauth/src/Endpoints/GitHubOauth.cs b/plugins/VNLib.Plugins.Essentials.SocialOauth/src/Endpoints/GitHubOauth.cs deleted file mode 100644 index e8abf5a..0000000 --- a/plugins/VNLib.Plugins.Essentials.SocialOauth/src/Endpoints/GitHubOauth.cs +++ /dev/null @@ -1,214 +0,0 @@ -/* -* Copyright (c) 2023 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Essentials.SocialOauth -* File: GitHubOauth.cs -* -* GitHubOauth.cs is part of VNLib.Plugins.Essentials.SocialOauth which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Essentials.SocialOauth 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.SocialOauth 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.Threading; -using System.Text.Json; -using System.Threading.Tasks; -using System.Collections.Generic; -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; - - -namespace VNLib.Plugins.Essentials.SocialOauth.Endpoints -{ - [ConfigurationName("github")] - 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() - .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("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 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(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 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(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(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/VNLib.Plugins.Essentials.SocialOauth/src/GetTokenRequest.cs b/plugins/VNLib.Plugins.Essentials.SocialOauth/src/GetTokenRequest.cs deleted file mode 100644 index 6e7635e..0000000 --- a/plugins/VNLib.Plugins.Essentials.SocialOauth/src/GetTokenRequest.cs +++ /dev/null @@ -1,34 +0,0 @@ -/* -* Copyright (c) 2023 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Essentials.SocialOauth -* File: GetTokenRequest.cs -* -* GetTokenRequest.cs is part of VNLib.Plugins.Essentials.SocialOauth which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Essentials.SocialOauth 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.SocialOauth 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.SocialOauth -{ - /// - /// A request message to get an OAuth2 token from a code - /// - /// The clients authentication code - /// The redirect url for current site - public sealed record class GetTokenRequest(string Code, string RedirectUrl) - { } -} diff --git a/plugins/VNLib.Plugins.Essentials.SocialOauth/src/IOAuthAccessState.cs b/plugins/VNLib.Plugins.Essentials.SocialOauth/src/IOAuthAccessState.cs deleted file mode 100644 index 888cc02..0000000 --- a/plugins/VNLib.Plugins.Essentials.SocialOauth/src/IOAuthAccessState.cs +++ /dev/null @@ -1,57 +0,0 @@ -/* -* Copyright (c) 2022 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Essentials.SocialOauth -* File: IOAuthAccessState.cs -* -* IOAuthAccessState.cs is part of VNLib.Plugins.Essentials.SocialOauth which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Essentials.SocialOauth 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.SocialOauth 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/. -*/ - -#nullable enable - -namespace VNLib.Plugins.Essentials.SocialOauth -{ - /// - /// An object that represents an OAuth2 access token in its - /// standard form. - /// - public interface IOAuthAccessState - { - /// - /// The OAuth2 access token - /// - public string? Token { get; set; } - /// - /// Token grant scope - /// - string? Scope { get; set; } - /// - /// The OAuth2 token type, usually 'Bearer' - /// - string? Type { get; set; } - /// - /// Optional refresh token - /// - string? RefreshToken { get; set; } - - /// - /// Optional ID OIDC token - /// - string? IdToken { get; set; } - } -} \ No newline at end of file diff --git a/plugins/VNLib.Plugins.Essentials.SocialOauth/src/LoginClaim.cs b/plugins/VNLib.Plugins.Essentials.SocialOauth/src/LoginClaim.cs deleted file mode 100644 index fa425cc..0000000 --- a/plugins/VNLib.Plugins.Essentials.SocialOauth/src/LoginClaim.cs +++ /dev/null @@ -1,73 +0,0 @@ -/* -* Copyright (c) 2023 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Essentials.SocialOauth -* File: LoginClaim.cs -* -* LoginClaim.cs is part of VNLib.Plugins.Essentials.SocialOauth which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Essentials.SocialOauth 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.SocialOauth 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.SocialOauth -{ - 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 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.SocialOauth/src/LoginUriBuilder.cs b/plugins/VNLib.Plugins.Essentials.SocialOauth/src/LoginUriBuilder.cs deleted file mode 100644 index 95334c6..0000000 --- a/plugins/VNLib.Plugins.Essentials.SocialOauth/src/LoginUriBuilder.cs +++ /dev/null @@ -1,127 +0,0 @@ -/* -* Copyright (c) 2023 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Essentials.SocialOauth -* File: LoginUriBuilder.cs -* -* LoginUriBuilder.cs is part of VNLib.Plugins.Essentials.SocialOauth which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Essentials.SocialOauth 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.SocialOauth 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.SocialOauth -{ - /* - * 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 scheme, ReadOnlySpan authority, ReadOnlySpan path) - { - //Alloc stack buffer for url - Span buffer = stackalloc char[1024]; - - //buffer writer for easier syntax - ForwardOnlyWriter 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 buffer = MemoryUtil.UnsafeAllocNearestPage(8000); - - Span binBuffer = buffer.Span[2048..]; - Span charBuffer = MemoryMarshal.Cast(buffer.Span[..2048]); - - - /* - * Build the character uri so we can encode it to binary, - * encrypt it and return it to the client - */ - - ForwardOnlyWriter 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 url = writer.AsSpan(); - - //Separate bin buffers for encryption and encoding - Span encryptionBuffer = binBuffer[1024..]; - Span 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.SocialOauth/src/OAuthSiteAdapter.cs b/plugins/VNLib.Plugins.Essentials.SocialOauth/src/OAuthSiteAdapter.cs deleted file mode 100644 index ce4f08c..0000000 --- a/plugins/VNLib.Plugins.Essentials.SocialOauth/src/OAuthSiteAdapter.cs +++ /dev/null @@ -1,72 +0,0 @@ -/* -* Copyright (c) 2023 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Essentials.SocialOauth -* File: OAuthSiteAdapter.cs -* -* OAuthSiteAdapter.cs is part of VNLib.Plugins.Essentials.SocialOauth which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Essentials.SocialOauth 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.SocialOauth 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.SocialOauth -{ - /// - /// Provides strucutred http messaging to an OAuth2 site. - /// - public sealed class OAuthSiteAdapter : RestSiteAdapterBase - { - protected override RestClientPool Pool { get; } - - /// - /// Initializes a new instance of the class. - /// - 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); - } - - /// - public override void OnResponse(RestResponse response) - { } - - /// - public override Task WaitAsync(CancellationToken cancellation = default) - { - return Task.CompletedTask; - } - } -} diff --git a/plugins/VNLib.Plugins.Essentials.SocialOauth/src/OauthClientConfig.cs b/plugins/VNLib.Plugins.Essentials.SocialOauth/src/OauthClientConfig.cs deleted file mode 100644 index 4e14063..0000000 --- a/plugins/VNLib.Plugins.Essentials.SocialOauth/src/OauthClientConfig.cs +++ /dev/null @@ -1,148 +0,0 @@ -/* -* Copyright (c) 2023 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Essentials.SocialOauth -* File: OauthClientConfig.cs -* -* OauthClientConfig.cs is part of VNLib.Plugins.Essentials.SocialOauth which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Essentials.SocialOauth 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.SocialOauth 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.SocialOauth -{ - - /// - /// Contains the standard configuration data for an OAuth2 endpoint - /// defined by plugin configuration - /// - 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); - }); - } - } - - /// - /// The client ID for the OAuth2 service - /// - public IAsyncLazy ClientID { get; } - - /// - /// The client secret for the OAuth2 service - /// - public IAsyncLazy ClientSecret { get; } - - - /// - /// The user-account origin value. Specifies that the user account - /// was created outside of the local account system - /// - public string AccountOrigin { get; } - - /// - /// The URL to redirect the user to the OAuth2 service - /// to begin the authentication process - /// - public Uri AccessCodeUrl { get; } - - /// - /// The remote endoint to exchange codes for access tokens - /// - public Uri AccessTokenUrl { get; } - - /// - /// The endpoint to get user-data object from - /// - public Uri UserDataUrl { get; } - - /// - /// The endpoint route/path - /// - public string EndpointPath { get; } - - /// - /// The size (in bytes) of the random generated nonce - /// - public uint NonceByteSize { get; } - - /// - /// A value that specifies if locally created accounts are allowed - /// to be logged in from an OAuth2 source - /// - public bool AllowForLocalAccounts { get; } - - /// - /// A value that indicates if accounts that do not exist will be created - /// and logged in immediatly, on successfull OAuth2 flow - /// - public bool AllowRegistration { get; } - - /// - /// The size (in bytes) of the random password generated for new users - /// - public int RandomPasswordSize { get; } - - /// - /// The initial time the login claim is valid for - /// - public TimeSpan InitClaimValidFor { get; } - } -} diff --git a/plugins/VNLib.Plugins.Essentials.SocialOauth/src/SocialEntryPoint.cs b/plugins/VNLib.Plugins.Essentials.SocialOauth/src/SocialEntryPoint.cs deleted file mode 100644 index 83e45c8..0000000 --- a/plugins/VNLib.Plugins.Essentials.SocialOauth/src/SocialEntryPoint.cs +++ /dev/null @@ -1,67 +0,0 @@ -/* -* Copyright (c) 2023 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Essentials.SocialOauth -* File: SocialEntryPoint.cs -* -* SocialEntryPoint.cs is part of VNLib.Plugins.Essentials.SocialOauth which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Essentials.SocialOauth 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.SocialOauth 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.Utils.Logging; -using VNLib.Plugins.Essentials.SocialOauth.Endpoints; -using VNLib.Plugins.Extensions.Loading; -using VNLib.Plugins.Extensions.Loading.Routing; - -namespace VNLib.Plugins.Essentials.SocialOauth -{ - public sealed class SocialEntryPoint : PluginBase - { - - public override string PluginName => "Essentials.SocialOauth"; - - protected override void OnLoad() - { - //Get the discord oauth config from the config file - if (this.HasConfigForType()) - { - //Add the discord login endpoint - this.Route(); - Log.Information("Discord social OAuth authentication loaded"); - } - if (this.HasConfigForType()) - { - //Add the github login endpoint - this.Route(); - Log.Information("Github social OAuth authentication loaded"); - } - } - - - protected override void OnUnLoad() - { - Log.Information("Plugin unloaded"); - } - - protected override void ProcessHostCommand(string cmd) - { - throw new NotImplementedException(); - } - } -} \ No newline at end of file diff --git a/plugins/VNLib.Plugins.Essentials.SocialOauth/src/SocialOauthBase.cs b/plugins/VNLib.Plugins.Essentials.SocialOauth/src/SocialOauthBase.cs deleted file mode 100644 index 561962a..0000000 --- a/plugins/VNLib.Plugins.Essentials.SocialOauth/src/SocialOauthBase.cs +++ /dev/null @@ -1,572 +0,0 @@ -/* -* Copyright (c) 2023 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Essentials.SocialOauth -* File: SocialOauthBase.cs -* -* SocialOauthBase.cs is part of VNLib.Plugins.Essentials.SocialOauth which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Essentials.SocialOauth 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.SocialOauth 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.SocialOauth.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.SocialOauth -{ - - /// - /// Provides a base class for derriving commong OAuth2 implicit authentication - /// - 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"; - - - /// - /// The client configuration struct passed during base class construction - /// - protected virtual OauthClientConfig Config { get; } - - /// - protected override ProtectionSettings EndpointProtectionSettings { get; } - - /// - /// The site adapter used to make requests to the OAuth2 provider - /// - protected OAuthSiteAdapter SiteAdapter { get; } - - /// - /// The user manager used to create and manage user accounts - /// - protected IUserManager Users { get; } - - private readonly IValidator ClaimValidator; - private readonly IValidator NonceValidator; - private readonly IValidator 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(config); - - //Init endpoint - InitPathAndLog(Config.EndpointPath, plugin.Log); - - Users = plugin.GetOrCreateSingleton(); - - - //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() - .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 GetClaimValidator() - { - InlineValidator 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 GetNonceValidator() - { - InlineValidator val = new(); - val.RuleFor(static s => s) - .Length(10, 200) - //Nonces are base32, so only alpha num - .AlphaNumeric(); - return val; - } - - /// - 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); - } - - /// - /// When derrived in a child class, exchanges an OAuth2 code grant type - /// for an OAuth2 access token to make api requests - /// - /// - /// The raw code from the remote OAuth2 granting server - /// A token to cancel the operation - /// - /// A task the resolves the that includes all relavent - /// authorization data. Result may be null if authorzation is invalid or not granted - /// - protected virtual async Task 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(); - - if(response?.Error != null) - { - Log.Debug("Error result from {conf} code {code} description: {err}", Config.AccountOrigin, response.Error, response.ErrorDescription); - return null; - } - - return response; - } - - /// - /// Gets an object that represents the user's account data from the OAuth provider when - /// creating a new user for the current platform - /// - /// The access state from the code/token exchange - /// A token to cancel the operation - /// The user's account data, null if not account exsits on the remote site, and process cannot continue - protected abstract Task GetAccountDataAsync(IOAuthAccessState clientAccess, CancellationToken cancellationToken); - - /// - /// Gets an object that represents the required information for logging-in a user (namley unique user-id) - /// - /// The authorization information granted from the OAuth2 authorization server - /// A token to cancel the operation - /// - protected abstract Task 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 - */ - - /// - protected override async ValueTask PutAsync(HttpEntity entity) - { - ValErrWebMessage webm = new(); - - //Get the login message - LoginClaim? claim = await entity.GetJsonFromFileAsync(); - - 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 - */ - - /// - protected override async ValueTask 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 - */ - - /// - protected override async ValueTask 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(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.SocialOauth/src/UserLoginData.cs b/plugins/VNLib.Plugins.Essentials.SocialOauth/src/UserLoginData.cs deleted file mode 100644 index 93f9f12..0000000 --- a/plugins/VNLib.Plugins.Essentials.SocialOauth/src/UserLoginData.cs +++ /dev/null @@ -1,34 +0,0 @@ -/* -* Copyright (c) 2023 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Essentials.SocialOauth -* File: UserLoginData.cs -* -* UserLoginData.cs is part of VNLib.Plugins.Essentials.SocialOauth which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Essentials.SocialOauth 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.SocialOauth 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.SocialOauth -{ - public class UserLoginData - { - [JsonPropertyName("user_id")] - public string? UserId { get; set; } - } -} diff --git a/plugins/VNLib.Plugins.Essentials.SocialOauth/src/VNLib.Plugins.Essentials.SocialOauth.csproj b/plugins/VNLib.Plugins.Essentials.SocialOauth/src/VNLib.Plugins.Essentials.SocialOauth.csproj deleted file mode 100644 index 1b014cc..0000000 --- a/plugins/VNLib.Plugins.Essentials.SocialOauth/src/VNLib.Plugins.Essentials.SocialOauth.csproj +++ /dev/null @@ -1,66 +0,0 @@ - - - - enable - net6.0 - VNLib.Plugins.Essentials.SocialOauth - SocialOauth - True - latest-all - en-US - true - - - - VNLib.Plugins.Essentials.SocialOauth - Vaughn Nugent - Vaughn Nugent - A basic external OAuth2 authentication plugin. - Copyright © 2023 Vaughn Nugent - https://www.vaughnnugent.com/resources/software/modules/Plugins.Essentials - https://github.com/VnUgE/Plugins.Essentials/tree/master/plugins/VNLib.Plugins.Essentials.SocialOauth - - Essentials framework plugin for common OAuth2 web-based client authentication. Currently implements GitHub, Discord, and Auth0 - authentication flows. - - - - - README.md - LICENSE - - - - True - \ - Always - - - True - \ - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - diff --git a/plugins/VNLib.Plugins.Essentials.SocialOauth/src/Validators/AccountDataValidator.cs b/plugins/VNLib.Plugins.Essentials.SocialOauth/src/Validators/AccountDataValidator.cs deleted file mode 100644 index 0ccda69..0000000 --- a/plugins/VNLib.Plugins.Essentials.SocialOauth/src/Validators/AccountDataValidator.cs +++ /dev/null @@ -1,83 +0,0 @@ -/* -* Copyright (c) 2023 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Essentials.SocialOauth -* File: AccountDataValidator.cs -* -* AccountDataValidator.cs is part of VNLib.Plugins.Essentials.SocialOauth which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Essentials.SocialOauth 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.SocialOauth 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.SocialOauth.Validators -{ - internal class AccountDataValidator : AbstractValidator - { - 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.SocialOauth/src/Validators/LoginMessageValidation.cs b/plugins/VNLib.Plugins.Essentials.SocialOauth/src/Validators/LoginMessageValidation.cs deleted file mode 100644 index 3cf4e70..0000000 --- a/plugins/VNLib.Plugins.Essentials.SocialOauth/src/Validators/LoginMessageValidation.cs +++ /dev/null @@ -1,65 +0,0 @@ -/* -* Copyright (c) 2023 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Essentials.SocialOauth -* File: LoginMessageValidation.cs -* -* LoginMessageValidation.cs is part of VNLib.Plugins.Essentials.SocialOauth which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Essentials.SocialOauth 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.SocialOauth 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.SocialOauth.Validators -{ - internal class LoginMessageValidation : AbstractValidator - { - /* - * 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"); - } - } -} -- cgit