aboutsummaryrefslogtreecommitdiff
path: root/VNLib.Plugins.Essentials.SocialOauth
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2023-01-12 17:47:40 -0500
committerLibravatar vnugent <public@vaughnnugent.com>2023-01-12 17:47:40 -0500
commit551066ed9a255bd47c1c5789ec1998fda64bd5aa (patch)
treed6caceb0e7caa44478c6611903b4b7e120964c89 /VNLib.Plugins.Essentials.SocialOauth
parentb6481038bc6573af30492e9ce52b36d9f64195f3 (diff)
Large project reorder and consolidation
Diffstat (limited to 'VNLib.Plugins.Essentials.SocialOauth')
-rw-r--r--VNLib.Plugins.Essentials.SocialOauth/ClientAccessTokenState.cs85
-rw-r--r--VNLib.Plugins.Essentials.SocialOauth/ClientRequestState.cs81
-rw-r--r--VNLib.Plugins.Essentials.SocialOauth/Endpoints/Auth0.cs195
-rw-r--r--VNLib.Plugins.Essentials.SocialOauth/Endpoints/DiscordOauth.cs158
-rw-r--r--VNLib.Plugins.Essentials.SocialOauth/Endpoints/GitHubOauth.cs219
-rw-r--r--VNLib.Plugins.Essentials.SocialOauth/IOAuthAccessState.cs57
-rw-r--r--VNLib.Plugins.Essentials.SocialOauth/OauthClientConfig.cs126
-rw-r--r--VNLib.Plugins.Essentials.SocialOauth/README.md45
-rw-r--r--VNLib.Plugins.Essentials.SocialOauth/SocialEntryPoint.cs82
-rw-r--r--VNLib.Plugins.Essentials.SocialOauth/SocialOauthBase.cs617
-rw-r--r--VNLib.Plugins.Essentials.SocialOauth/UserLoginData.cs36
-rw-r--r--VNLib.Plugins.Essentials.SocialOauth/VNLib.Plugins.Essentials.SocialOauth.csproj54
-rw-r--r--VNLib.Plugins.Essentials.SocialOauth/Validators/AccountDataValidator.cs74
-rw-r--r--VNLib.Plugins.Essentials.SocialOauth/Validators/LoginMessageValidation.cs60
14 files changed, 0 insertions, 1889 deletions
diff --git a/VNLib.Plugins.Essentials.SocialOauth/ClientAccessTokenState.cs b/VNLib.Plugins.Essentials.SocialOauth/ClientAccessTokenState.cs
deleted file mode 100644
index e5de597..0000000
--- a/VNLib.Plugins.Essentials.SocialOauth/ClientAccessTokenState.cs
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
-* Copyright (c) 2022 Vaughn Nugent
-*
-* Library: VNLib
-* Package: VNLib.Plugins.Essentials.SocialOauth
-* File: ClientAccessTokenState.cs
-*
-* ClientAccessTokenState.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 System.Text.Json.Serialization;
-
-using VNLib.Hashing;
-using VNLib.Utils.Memory;
-using VNLib.Utils.Memory.Caching;
-using VNLib.Plugins.Essentials.Accounts;
-
-namespace VNLib.Plugins.Essentials.SocialOauth
-{
- public sealed class OAuthAccessState : IOAuthAccessState, ICacheable, INonce
- {
- ///<inheritdoc/>
- [JsonPropertyName("access_token")]
- public string? Token { get; set; }
- ///<inheritdoc/>
- [JsonPropertyName("scope")]
- public string? Scope { get; set; }
- ///<inheritdoc/>
- [JsonPropertyName("token_type")]
- public string? Type { get; set; }
- ///<inheritdoc/>
- [JsonPropertyName("refresh_token")]
- public string? RefreshToken { get; set; }
- ///<inheritdoc/>
- [JsonPropertyName("id_token")]
- public string? IdToken { get; set; }
-
- //Ignore the public key and client ids
- [JsonIgnore]
- internal string? PublicKey { get; set; }
- [JsonIgnore]
- internal string? ClientId { get; set; }
-
- /// <summary>
- /// A random nonce generated when the access state is created and
- /// deleted when then access token is evicted.
- /// </summary>
- [JsonIgnore]
- internal ReadOnlyMemory<byte> Nonce { get; private set; }
-
- DateTime ICacheable.Expires { get; set; }
- bool IEquatable<ICacheable>.Equals(ICacheable? other) => GetHashCode() == other?.GetHashCode();
- public override int GetHashCode() => Token!.GetHashCode(StringComparison.Ordinal);
- void ICacheable.Evicted()
- {
- Memory.UnsafeZeroMemory(Nonce);
- }
-
- void INonce.ComputeNonce(Span<byte> buffer)
- {
- //Compute nonce
- RandomHash.GetRandomBytes(buffer);
- //Copy and store
- Nonce = buffer.ToArray();
- }
-
- bool INonce.VerifyNonce(ReadOnlySpan<byte> nonceBytes) => CryptographicOperations.FixedTimeEquals(Nonce.Span, nonceBytes);
- }
-} \ No newline at end of file
diff --git a/VNLib.Plugins.Essentials.SocialOauth/ClientRequestState.cs b/VNLib.Plugins.Essentials.SocialOauth/ClientRequestState.cs
deleted file mode 100644
index 2f35e48..0000000
--- a/VNLib.Plugins.Essentials.SocialOauth/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<byte> _rawKey;
-
- /// <summary>
- /// The raw nonce state bytes
- /// </summary>
- public ReadOnlyMemory<byte> State { get; private set; }
-
- public ClientRequestState(ReadOnlySpan<char> keyChar, int nonceBytes)
- {
- //Get browser id
- _rawKey = Convert.FromHexString(keyChar);
- RecomputeState(nonceBytes);
- }
-
- /// <summary>
- /// Recomputes a nonce state and signature for the current
- /// connection
- /// </summary>
- /// <param name="nonceBytes">The size of the nonce (in bytes) to generate</param>
- public void RecomputeState(int nonceBytes)
- {
- //Get random nonce buffer
- State = RandomHash.GetRandomBytes(nonceBytes);
- }
- /// <summary>
- /// Computes the signature of the supplied data based on the original
- /// client state for this connection
- /// </summary>
- /// <param name="data"></param>
- /// <returns></returns>
- public ERRNO ComputeSignatureForClient(ReadOnlySpan<byte> data, Span<byte> output)
- {
- return HMACSHA512.TryHashData(_rawKey.Span, data, output, out int count) ? count : ERRNO.E_FAIL;
- }
-
- public DateTime Expires { get; set; }
- bool IEquatable<ICacheable>.Equals(ICacheable other) => ReferenceEquals(this, other);
- void ICacheable.Evicted()
- {
- //Zero secrets on eviction
- Memory.UnsafeZeroMemory(State);
- Memory.UnsafeZeroMemory(_rawKey);
- }
- }
-} \ No newline at end of file
diff --git a/VNLib.Plugins.Essentials.SocialOauth/Endpoints/Auth0.cs b/VNLib.Plugins.Essentials.SocialOauth/Endpoints/Auth0.cs
deleted file mode 100644
index c7512b7..0000000
--- a/VNLib.Plugins.Essentials.SocialOauth/Endpoints/Auth0.cs
+++ /dev/null
@@ -1,195 +0,0 @@
-/*
-* Copyright (c) 2022 Vaughn Nugent
-*
-* Library: VNLib
-* Package: VNLib.Plugins.Essentials.SocialOauth
-* File: Auth0.cs
-*
-* Auth0.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.Linq;
-using System.Text;
-using System.Text.Json;
-using System.Threading;
-using System.Threading.Tasks;
-using System.Collections.Generic;
-
-using RestSharp;
-
-using VNLib.Net.Rest.Client;
-using VNLib.Hashing;
-using VNLib.Hashing.IdentityUtility;
-using VNLib.Utils.Logging;
-using VNLib.Plugins.Essentials.Accounts;
-using VNLib.Plugins.Extensions.Loading;
-using VNLib.Plugins.Extensions.Loading.Users;
-
-namespace VNLib.Plugins.Essentials.SocialOauth.Endpoints
-{
-
- [ConfigurationName("auth0")]
- internal sealed class Auth0 : SocialOauthBase
- {
-
- protected override OauthClientConfig Config { get; }
-
-
- private readonly Task<JsonDocument> Auth0VerificationJwk;
-
- public Auth0(PluginBase plugin, IReadOnlyDictionary<string, JsonElement> config) : base()
- {
- string keyUrl = config["key_url"].GetString() ?? throw new KeyNotFoundException("Missing Auth0 'key_url' from config");
-
- Uri keyUri = new(keyUrl);
-
- //Get certificate on background thread
- Auth0VerificationJwk = Task.Run(() => GetRsaCertificate(keyUri));
-
- Config = new("auth0", config)
- {
- Passwords = plugin.GetPasswords(),
- Users = plugin.GetUserManager(),
- };
-
- InitPathAndLog(Config.EndpointPath, plugin.Log);
-
- //Load secrets
- _ = plugin.DeferTask(async () =>
- {
- //Get id/secret
- Task<SecretResult?> secretTask = plugin.TryGetSecretAsync("auth0_client_secret");
- Task<SecretResult?> clientIdTask = plugin.TryGetSecretAsync("auth0_client_id");
-
- await Task.WhenAll(secretTask, clientIdTask);
-
- using SecretResult? secret = await secretTask;
- using SecretResult? clientId = await clientIdTask;
-
- Config.ClientID = clientId?.Result.ToString() ?? throw new KeyNotFoundException("Missing Auth0 client id from config or vault");
- Config.ClientSecret = secret?.Result.ToString() ?? throw new KeyNotFoundException("Missing the Auth0 client secret from config or vault");
-
- }, 100);
- }
-
-
- private async Task<JsonDocument> GetRsaCertificate(Uri certUri)
- {
- try
- {
- Log.Debug("Getting Auth0 signing keys");
- //Get key request
- RestRequest keyRequest = new(certUri, Method.Get);
- keyRequest.AddHeader("Accept", "application/json");
-
- //rent client from pool
- using ClientContract client = ClientPool.Lease();
-
- RestResponse response = await client.Resource.ExecuteAsync(keyRequest);
-
- response.ThrowIfError();
-
- return JsonDocument.Parse(response.RawBytes);
- }
- catch (Exception e)
- {
- Log.Error(e, "Failed to get Auth0 signing keys");
- throw;
- }
- }
-
- /*
- * 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
- */
- protected override Task<AccountData?> GetAccountDataAsync(IOAuthAccessState clientAccess, CancellationToken cancellationToken)
- {
- using JsonWebToken jwt = JsonWebToken.Parse(clientAccess.IdToken);
-
- //verify signature
-
- using JsonDocument userData = jwt.GetPayload();
-
- if (!userData.RootElement.GetProperty("email_verified").GetBoolean())
- {
- return Task.FromResult<AccountData?>(null);
- }
-
- string fullName = userData.RootElement.GetProperty("name").GetString() ?? " ";
-
- return Task.FromResult<AccountData?>(new AccountData()
- {
- EmailAddress = userData.RootElement.GetProperty("email").GetString(),
- First = fullName.Split(' ')[0],
- Last = fullName.Split(' ')[1],
- });
- }
-
- private static string GetUserIdFromPlatform(string userName)
- {
- /*
- * Auth0 uses the format "platoform|{user_id}" for the user id so it should match the
- * external platofrm as github and discord endoints also
- */
-
- return ManagedHash.ComputeHash(userName, HashAlg.SHA1, HashEncodingMode.Hexadecimal);
- }
-
-
- private static readonly Task<UserLoginData?> EmptyLoginData = Task.FromResult<UserLoginData?>(null);
-
- protected override Task<UserLoginData?> GetLoginDataAsync(IOAuthAccessState clientAccess, CancellationToken cancellation)
- {
- using JsonWebToken jwt = JsonWebToken.Parse(clientAccess.IdToken);
-
- //Verify the token against the first signing key
- if (!jwt.VerifyFromJwk(Auth0VerificationJwk.Result.RootElement.GetProperty("keys").EnumerateArray().First()))
- {
- 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.Equals(audience, StringComparison.Ordinal))
- {
- //Invalid audience
- return EmptyLoginData;
- }
-
- return Task.FromResult<UserLoginData?>(new UserLoginData()
- {
- UserId = GetUserIdFromPlatform(userId)
- });
- }
- }
-}
diff --git a/VNLib.Plugins.Essentials.SocialOauth/Endpoints/DiscordOauth.cs b/VNLib.Plugins.Essentials.SocialOauth/Endpoints/DiscordOauth.cs
deleted file mode 100644
index d8b2394..0000000
--- a/VNLib.Plugins.Essentials.SocialOauth/Endpoints/DiscordOauth.cs
+++ /dev/null
@@ -1,158 +0,0 @@
-/*
-* Copyright (c) 2022 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;
-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.Net.Rest.Client;
-using VNLib.Plugins.Essentials.Accounts;
-using VNLib.Plugins.Extensions.Loading;
-using VNLib.Plugins.Extensions.Loading.Users;
-
-
-namespace VNLib.Plugins.Essentials.SocialOauth.Endpoints
-{
- [ConfigurationName("discord")]
- internal sealed class DiscordOauth : SocialOauthBase
- {
- protected override OauthClientConfig Config { get; }
-
- public DiscordOauth(PluginBase plugin, IReadOnlyDictionary<string, JsonElement> config) : base()
- {
- Config = new("discord", config)
- {
- Passwords = plugin.GetPasswords(),
- Users = plugin.GetUserManager(),
- };
-
- InitPathAndLog(Config.EndpointPath, plugin.Log);
-
- //Load secrets
- _ = plugin.DeferTask(async () =>
- {
- //Get id/secret
- Task<SecretResult?> clientIdTask = plugin.TryGetSecretAsync("discord_client_id");
- Task<SecretResult?> secretTask = plugin.TryGetSecretAsync("discord_client_secret");
-
- await Task.WhenAll(secretTask, clientIdTask);
-
- using SecretResult? secret = await secretTask;
- using SecretResult? clientId = await clientIdTask;
-
- Config.ClientID = clientId?.Result.ToString() ?? throw new KeyNotFoundException("Missing Discord client id from config or vault");
- Config.ClientSecret = secret?.Result.ToString() ?? throw new KeyNotFoundException("Missing the Discord client secret from config or vault");
-
- }, 100);
- }
-
-
- private static string GetUserIdFromPlatform(string userName)
- {
- return ManagedHash.ComputeHash($"discord|{userName}", HashAlg.SHA1, HashEncodingMode.Hexadecimal);
- }
-
-
- /*
- * 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; }
- }
-
-
- protected override async Task<AccountData?> GetAccountDataAsync(IOAuthAccessState accessToken, CancellationToken cancellationToken)
- {
- //Get the user's email address's
- RestRequest request = new(Config.UserDataUrl);
- //Add authorization token
- request.AddHeader("Authorization", $"{accessToken.Type} {accessToken.Token}");
- //Get client from pool
- using ClientContract client = ClientPool.Lease();
- //get user's profile data
- RestResponse<UserProfile> getProfileResponse = await client.Resource.ExecuteAsync<UserProfile>(request, cancellationToken: cancellationToken);
- //Check response
- if (!getProfileResponse.IsSuccessful || getProfileResponse.Data == null)
- {
- Log.Debug("Discord user request responded with code {code}:{data}", getProfileResponse.StatusCode, getProfileResponse.Content);
- return null;
- }
- UserProfile discordProfile = getProfileResponse.Data;
- //Make sure the user's account is verified
- if (!discordProfile.Verified)
- {
- return null;
- }
- return new()
- {
- EmailAddress = discordProfile.EmailAddress,
- First = discordProfile.Username,
- };
- }
-
- protected override async Task<UserLoginData?> GetLoginDataAsync(IOAuthAccessState accessToken, CancellationToken cancellationToken)
- {
- //Get the user's email address's
- RestRequest request = new(Config.UserDataUrl);
- //Add authorization token
- request.AddHeader("Authorization", $"{accessToken.Type} {accessToken.Token}");
- //Get client from pool
- using ClientContract client = ClientPool.Lease();
- //get user's profile data
- RestResponse<UserProfile> getProfileResponse = await client.Resource.ExecuteAsync<UserProfile>(request, cancellationToken: cancellationToken);
- //Check response
- if (!getProfileResponse.IsSuccessful || getProfileResponse.Data?.UserID == null)
- {
- Log.Debug("Discord user request responded with code {code}:{data}", getProfileResponse.StatusCode, getProfileResponse.Content);
- return null;
- }
-
- UserProfile discordProfile = getProfileResponse.Data;
-
- return new()
- {
- //Get unique user-id from the discord profile and sha1 hex hash to store in db
- UserId = GetUserIdFromPlatform(discordProfile.UserID)
- };
- }
- }
-} \ No newline at end of file
diff --git a/VNLib.Plugins.Essentials.SocialOauth/Endpoints/GitHubOauth.cs b/VNLib.Plugins.Essentials.SocialOauth/Endpoints/GitHubOauth.cs
deleted file mode 100644
index 676f2bb..0000000
--- a/VNLib.Plugins.Essentials.SocialOauth/Endpoints/GitHubOauth.cs
+++ /dev/null
@@ -1,219 +0,0 @@
-/*
-* Copyright (c) 2022 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.Text;
-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.Net.Rest.Client;
-using VNLib.Plugins.Essentials.Accounts;
-using VNLib.Plugins.Extensions.Loading;
-using VNLib.Plugins.Extensions.Loading.Users;
-
-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;
-
- protected override OauthClientConfig Config { get; }
-
- public GitHubOauth(PluginBase plugin, IReadOnlyDictionary<string, JsonElement> config) : base()
- {
-
- UserEmailUrl = config["user_email_url"].GetString() ?? throw new KeyNotFoundException("Missing required key 'user_email_url' for github configuration");
-
- Config = new("github", config)
- {
- Passwords = plugin.GetPasswords(),
- Users = plugin.GetUserManager(),
- };
-
- InitPathAndLog(Config.EndpointPath, plugin.Log);
-
- //Load secrets
- _ = plugin.DeferTask(async () =>
- {
- //Get id/secret
- Task<SecretResult?> clientIdTask = plugin.TryGetSecretAsync("github_client_id");
- Task<SecretResult?> secretTask = plugin.TryGetSecretAsync("github_client_secret");
-
- await Task.WhenAll(secretTask, clientIdTask);
-
- using SecretResult? secret = await secretTask;
- using SecretResult? clientId = await clientIdTask;
-
- Config.ClientID = clientId?.Result.ToString() ?? throw new KeyNotFoundException("Missing Github client id from config or vault");
- Config.ClientSecret = secret?.Result.ToString() ?? throw new KeyNotFoundException("Missing the Github client secret from config or vault");
-
- }, 100);
- }
-
- protected override void StaticClientPoolInitializer(RestClient client)
- {
- client.UseSerializer<RestSharp.Serializers.Json.SystemTextJsonSerializer>();
- //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; }
- }
- /*
- * 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; }
- }
-
- private static string GetUserIdFromPlatform(int userId)
- {
- return ManagedHash.ComputeHash($"github|{userId}", HashAlg.SHA1, HashEncodingMode.Hexadecimal);
- }
-
- protected override async Task<UserLoginData?> 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();
-
- //Exec the get for the profile
- RestResponse<GithubProfile> profResponse = await client.Resource.ExecuteAsync<GithubProfile>(request, cancellationToken);
-
- if (!profResponse.IsSuccessful || profResponse.Data == null || profResponse.Data.ID < 100)
- {
- Log.Debug("Github login data attempt responded with status code {code}", profResponse.StatusCode);
- return null;
- }
-
- //Return login data
- return new()
- {
- //User-id is just the SHA 1
- UserId = GetUserIdFromPlatform(profResponse.Data.ID)
- };
- }
-
- protected override async Task<AccountData?> 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 user's emails
- RestResponse<EmailContainer[]> getEmailResponse = await client.Resource.ExecuteAsync<EmailContainer[]>(request, cancellationToken: cancellationToken);
- //Check status
- if (getEmailResponse.IsSuccessful && getEmailResponse.Data != null)
- {
- //Filter emails addresses
- foreach (EmailContainer email in getEmailResponse.Data)
- {
- //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 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<GithubProfile> profResponse = await client.Resource.ExecuteAsync<GithubProfile>(request, cancellationToken);
- if (!profResponse.IsSuccessful || profResponse.Data == null)
- {
- Log.Debug("Github account data request failed but GH responded with status code {code}", profResponse.StatusCode);
- return null;
- }
-
- //Get the user's name from gh profile
- string[] names = profResponse.Data.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;
- }
-
- }
-} \ No newline at end of file
diff --git a/VNLib.Plugins.Essentials.SocialOauth/IOAuthAccessState.cs b/VNLib.Plugins.Essentials.SocialOauth/IOAuthAccessState.cs
deleted file mode 100644
index 888cc02..0000000
--- a/VNLib.Plugins.Essentials.SocialOauth/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
-{
- /// <summary>
- /// An object that represents an OAuth2 access token in its
- /// standard form.
- /// </summary>
- public interface IOAuthAccessState
- {
- /// <summary>
- /// The OAuth2 access token
- /// </summary>
- public string? Token { get; set; }
- /// <summary>
- /// Token grant scope
- /// </summary>
- string? Scope { get; set; }
- /// <summary>
- /// The OAuth2 token type, usually 'Bearer'
- /// </summary>
- string? Type { get; set; }
- /// <summary>
- /// Optional refresh token
- /// </summary>
- string? RefreshToken { get; set; }
-
- /// <summary>
- /// Optional ID OIDC token
- /// </summary>
- string? IdToken { get; set; }
- }
-} \ No newline at end of file
diff --git a/VNLib.Plugins.Essentials.SocialOauth/OauthClientConfig.cs b/VNLib.Plugins.Essentials.SocialOauth/OauthClientConfig.cs
deleted file mode 100644
index 9caf705..0000000
--- a/VNLib.Plugins.Essentials.SocialOauth/OauthClientConfig.cs
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
-* Copyright (c) 2022 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.Text.Json;
-using System.Collections.Generic;
-
-using VNLib.Utils.Extensions;
-using VNLib.Plugins.Essentials.Users;
-using VNLib.Plugins.Essentials.Accounts;
-
-
-namespace VNLib.Plugins.Essentials.SocialOauth
-{
-
- public sealed class OauthClientConfig
- {
-
- public OauthClientConfig(string configName, IReadOnlyDictionary<string, JsonElement> config)
- {
- EndpointPath = config["path"].GetString() ?? throw new KeyNotFoundException($"Missing required key 'path' in config {configName}");
-
- //Set discord account origin
- AccountOrigin = config["account_origin"].GetString() ?? throw new KeyNotFoundException($"Missing required key 'account_origin' in config {configName}");
-
- //Get the auth and token urls
- string authUrl = config["authorization_url"].GetString() ?? throw new KeyNotFoundException($"Missing required key 'authorization_url' in config {configName}");
- string tokenUrl = config["token_url"].GetString() ?? throw new KeyNotFoundException($"Missing required key 'token_url' in config {configName}");
- string userUrl = config["user_data_url"].GetString() ?? throw new KeyNotFoundException($"Missing required key 'user_data_url' in config {configName}");
- //Create the uris
- AccessCodeUrl = new(authUrl);
- AccessTokenUrl = new(tokenUrl);
- UserDataUrl = new(userUrl);
-
- AllowForLocalAccounts = config["allow_for_local"].GetBoolean();
- AllowRegistration = config["allow_registration"].GetBoolean();
- LoginNonceLifetime = config["valid_for_sec"].GetTimeSpan(TimeParseType.Seconds);
- NonceByteSize = config["nonce_size"].GetUInt32();
- RandomPasswordSize = config["password_size"].GetInt32();
- }
-
-
- public string ClientID { get; set; } = string.Empty;
-
- public string ClientSecret { get; set; } = string.Empty;
-
-
- /// <summary>
- /// The user-account origin value. Specifies that the user account
- /// was created outside of the local account system
- /// </summary>
- public string AccountOrigin { get; }
-
- /// <summary>
- /// The URL to redirect the user to the OAuth2 service
- /// to begin the authentication process
- /// </summary>
- public Uri AccessCodeUrl { get; }
-
- /// <summary>
- /// The remote endoint to exchange codes for access tokens
- /// </summary>
- public Uri AccessTokenUrl { get; }
-
- /// <summary>
- /// The endpoint to get user-data object from
- /// </summary>
- public Uri UserDataUrl { get; }
-
- public TimeSpan LoginNonceLifetime { get; }
- /// <summary>
- /// The user store to create/get users from
- /// </summary>
- public IUserManager Users { get; init; }
-
- public PasswordHashing Passwords { get; init; }
-
- /// <summary>
- /// The endpoint route/path
- /// </summary>
- public string EndpointPath { get; }
-
- /// <summary>
- /// The size (in bytes) of the random generated nonce
- /// </summary>
- public uint NonceByteSize { get; }
-
- /// <summary>
- /// A value that specifies if locally created accounts are allowed
- /// to be logged in from an OAuth2 source
- /// </summary>
- public bool AllowForLocalAccounts { get; }
-
- /// <summary>
- /// A value that indicates if accounts that do not exist will be created
- /// and logged in immediatly, on successfull OAuth2 flow
- /// </summary>
- public bool AllowRegistration { get; }
-
- /// <summary>
- /// The size (in bytes) of the random password generated for new users
- /// </summary>
- public int RandomPasswordSize { get; }
- }
-}
diff --git a/VNLib.Plugins.Essentials.SocialOauth/README.md b/VNLib.Plugins.Essentials.SocialOauth/README.md
deleted file mode 100644
index 7a54c20..0000000
--- a/VNLib.Plugins.Essentials.SocialOauth/README.md
+++ /dev/null
@@ -1,45 +0,0 @@
-# VNLib.Plugins.Essentials.SocialOauth
-
-A basic external OAuth2 authentication plugin.
-
-## Plugin Mode
-
-This library exports an IPlugin type that may be loaded directly by a host application, or
-imported to provide base classes for creating OAuth2 authentication endpoints.
-
-By default, exports 2 endpoints for Github and Discord authentication. Configuration
-variables for either endpoint may be omitted or included to export endpoints.
-
-## Library Mode
-
-Exports SocialOAuthBase to provide a base class for creating OAuth2 authentication
-endpoints, that is compatible with the VNLib web client library authentication flow
-
-
-## Authentication Flow
-
-The authentication flow works similar to the local account mechanism with an extra step that helps
-guard against replay, and MITM attacks. When an request claim is made (request to login) from client
-side code (via put request), a browser id is request (for login flow) along with the clients encryption
-public key (same key as Essentials.Accounts requires). The public key is used to encrypted a derived
-redirect url, which includes a "secret" state token (OAuth2 standard state) that only the private-key
-holder should be able to recover. When decrypted, should be used to redirect the client's browser to
-the remote authentication server. Assuming the request is granted, the browser is redirected to the
-originating endpoint, and the nonce is used to recover the initial claim and the flow continues. The
-request should also include the required OAuth2 'code' parameter used to exchange for an access token.
-If the access token is granted, a nonce is generated, passed to the browser via a redirect query parameter
-which the browser code will use in a POST request to the endpoint to continue the flow. The nonce is
-used to recover the access token and original claim data (public key, browser id, etc), which is used
-to recover a user account, or optionally create a new account. Once complete, the user account is used
-to upgrade the session and grant authorization to the client. The public key (and browser id) is used
-from the initial claim to authorize the session, which should guard against MITM, replay, and forgery
-attacks. However this only works if we assume the clients private key has not been stolen, which is a
-much larger issue and should be addressed separately.
-
-## Diagram
-
-PUT -> { public_key, browser_id } -> server -> { result: "base64 encrypted redirect url"} ->
- OAuth2Server -> redirect -> "?code=some_code&state=decrypted_state_token"
-
-GET -> "?code=some_code&state=decrypted_state_token" -> server -> "?result=authorized&nonce=some_nonce"
-POST -> { nonce:"some_nonce" } -> server -> [authorization complete message] \ No newline at end of file
diff --git a/VNLib.Plugins.Essentials.SocialOauth/SocialEntryPoint.cs b/VNLib.Plugins.Essentials.SocialOauth/SocialEntryPoint.cs
deleted file mode 100644
index d0f7a84..0000000
--- a/VNLib.Plugins.Essentials.SocialOauth/SocialEntryPoint.cs
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
-* Copyright (c) 2022 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 System.Collections.Generic;
-
-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()
- {
- try
- {
- //Get the discord oauth config from the config file
- if (this.HasConfigForType<DiscordOauth>())
- {
- //Add the discord login endpoint
- this.Route<DiscordOauth>();
- Log.Information("Discord social OAuth authentication loaded");
- }
- if (this.HasConfigForType<GitHubOauth>())
- {
- //Add the github login endpoint
- this.Route<GitHubOauth>();
- Log.Information("Github social OAuth authentication loaded");
- }
-
- if (this.HasConfigForType<Auth0>())
- {
- //Add the auth0 login endpoint
- this.Route<Auth0>();
- Log.Information("Auth0 social OAuth authentication loaded");
- }
- }
- catch(KeyNotFoundException kne)
- {
- Log.Error("Missing required configuration variables, {reason}", kne.Message);
- }
- }
-
-
- 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/VNLib.Plugins.Essentials.SocialOauth/SocialOauthBase.cs b/VNLib.Plugins.Essentials.SocialOauth/SocialOauthBase.cs
deleted file mode 100644
index 6815bf3..0000000
--- a/VNLib.Plugins.Essentials.SocialOauth/SocialOauthBase.cs
+++ /dev/null
@@ -1,617 +0,0 @@
-/*
-* Copyright (c) 2022 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;
-using System.Text.Json;
-using System.Threading;
-using System.Threading.Tasks;
-using System.Collections.Generic;
-using System.Security.Cryptography;
-using System.Text.Json.Serialization;
-using System.Runtime.InteropServices;
-
-using FluentValidation;
-
-using RestSharp;
-using VNLib.Net.Http;
-using VNLib.Net.Rest.Client;
-using VNLib.Hashing;
-using VNLib.Utils;
-using VNLib.Utils.Memory;
-using VNLib.Utils.Logging;
-using VNLib.Utils.Extensions;
-using VNLib.Utils.Memory.Caching;
-using VNLib.Plugins.Essentials.Users;
-using VNLib.Plugins.Essentials.Accounts;
-using VNLib.Plugins.Essentials.Endpoints;
-using VNLib.Plugins.Essentials.Extensions;
-using VNLib.Plugins.Extensions.Validation;
-using VNLib.Plugins.Essentials.SocialOauth.Validators;
-
-namespace VNLib.Plugins.Essentials.SocialOauth
-{
-
- /// <summary>
- /// Provides a base class for derriving commong OAuth2 implicit authentication
- /// </summary>
- public abstract class SocialOauthBase : UnprotectedWebEndpoint
- {
- const string AUTH_ERROR_MESSAGE = "You have no pending authentication requests.";
-
- const string AUTH_GRANT_SESSION_NAME = "auth";
-
- /// <summary>
- /// The client configuration struct passed during base class construction
- /// </summary>
- protected abstract OauthClientConfig Config { get; }
-
- ///<inheritdoc/>
- protected override ProtectionSettings EndpointProtectionSettings { get; } = new()
- {
- /*
- * Disable cross site checking because the OAuth2 flow requires
- * cross site when redirecting the client back
- */
- DisableCrossSiteDenied = true
- };
-
- /// <summary>
- /// The resst client connection pool
- /// </summary>
- protected RestClientPool ClientPool { get; }
-
- private readonly Dictionary<string, LoginClaim> ClaimStore;
- private readonly Dictionary<string, OAuthAccessState> AuthorizationStore;
- private readonly IValidator<LoginClaim> ClaimValidator;
- private readonly IValidator<string> NonceValidator;
- private readonly IValidator<AccountData> AccountDataValidator;
-
- protected SocialOauthBase()
- {
- ClaimStore = new(StringComparer.OrdinalIgnoreCase);
- AuthorizationStore = new(StringComparer.OrdinalIgnoreCase);
- ClaimValidator = GetClaimValidator();
- NonceValidator = GetNonceValidator();
- AccountDataValidator = new AccountDataValidator();
-
- 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
- ClientPool = new(10, poolOptions, StaticClientPoolInitializer);
- }
-
- private static IValidator<LoginClaim> GetClaimValidator()
- {
- InlineValidator<LoginClaim> val = new();
- val.RuleFor(static s => s.ClientId)
- .Length(10, 100)
- .WithMessage("Request is not valid");
-
- val.RuleFor(static s => s.PublicKey)
- .Length(50, 1024)
- .WithMessage("Request is not valid");
-
- return val;
- }
- private static IValidator<string> GetNonceValidator()
- {
- InlineValidator<string> val = new();
- val.RuleFor(static s => s)
- .Length(10, 200)
- //Nonces are base32, so only alpha num
- .AlphaNumeric();
- return val;
- }
-
- 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
- if(entity.LoginCookieMatches() || entity.TokenMatches())
- {
- return false;
- }
- return true;
- }
-
- /// <summary>
- /// Invoked by the constructor during rest client initlialization
- /// </summary>
- /// <param name="client">The new client to be configured</param>
- protected virtual void StaticClientPoolInitializer(RestClient client)
- {
- client.AddDefaultHeader("accept", HttpHelpers.GetContentTypeString(ContentType.Json));
- client.UseSerializer<RestSharp.Serializers.Json.SystemTextJsonSerializer>();
- }
-
- protected virtual void OnBeforeGetToken(HttpEntity entity, string code, RestRequest state) { }
-
- /// <summary>
- /// When derrived in a child class, exchanges an OAuth2 code grant type
- /// for an OAuth2 access token to make api requests
- /// </summary>
- /// <param name="ev"></param>
- /// <param name="code">The raw code from the remote OAuth2 granting server</param>
- /// <param name="cancellationToken">A token to cancel the operation</param>
- /// <returns>
- /// A task the resolves the <see cref="OAuthAccessState"/> that includes all relavent
- /// authorization data. Result may be null if authorzation is invalid or not granted
- /// </returns>
- /// <param name="cancellationToken"></param>
- protected async Task<OAuthAccessState?> ExchangeCodeForTokenAsync(HttpEntity ev, string code, CancellationToken cancellationToken)
- {
- //valid response, time to get the actual authorization from gh for client
- RestRequest request = new(Config.AccessTokenUrl, Method.Post);
-
- //Add required params url-encoded
- request.AddParameter("client_id", Config.ClientID, ParameterType.GetOrPost);
- request.AddParameter("client_secret", Config.ClientSecret, ParameterType.GetOrPost);
- request.AddParameter("grant_type", "authorization_code", ParameterType.GetOrPost);
- request.AddParameter("code", code, ParameterType.GetOrPost);
- request.AddParameter("redirect_uri", $"{ev.Server.RequestUri.Scheme}://{ev.Server.RequestUri.Authority}{Path}", ParameterType.GetOrPost);
-
- //Allow reconfiguration
- OnBeforeGetToken(ev, code, request);
-
- //Get client from pool
- using ClientContract client = ClientPool.Lease();
- //Execute request and attempt to recover the authorization response
- RestResponse<OAuthAccessState> response = await client.Resource.ExecuteAsync<OAuthAccessState>(request, cancellationToken: cancellationToken);
- //Make sure successfull, if so return the access token to store
- return response.IsSuccessful && response.Data != null ? response.Data : null;
- }
-
- /// <summary>
- /// Gets an object that represents the user's account data from the OAuth provider when
- /// creating a new user for the current platform
- /// </summary>
- /// <param name="clientAccess">The access state from the code/token exchange</param>
- /// <param name="cancellationToken">A token to cancel the operation</param>
- /// <returns>The user's account data, null if not account exsits on the remote site, and process cannot continue</returns>
- /// <param name="cancellationToken"></param>
- protected abstract Task<AccountData?> GetAccountDataAsync(IOAuthAccessState clientAccess, CancellationToken cancellationToken);
- /// <summary>
- /// Gets an object that represents the required information for logging-in a user (namley unique user-id)
- /// </summary>
- /// <param name="clientAccess">The authorization information granted from the OAuth2 authorization server</param>
- /// <param name="cancellation">A token to cancel the operation</param>
- /// <returns></returns>
- protected abstract Task<UserLoginData?> GetLoginDataAsync(IOAuthAccessState clientAccess, CancellationToken cancellation);
-
- class LoginClaim : ICacheable, INonce
- {
- [JsonPropertyName("public_key")]
- public string? PublicKey { get; set; }
- [JsonPropertyName("browser_id")]
- public string? ClientId { get; set; }
-
- /// <summary>
- /// The raw OAuth flow state parameter the client must decrypt before
- /// navigating to remote authentication source
- /// </summary>
- [JsonIgnore]
- public ReadOnlyMemory<byte> RawNonce { get; private set; }
- [JsonIgnore]
- DateTime ICacheable.Expires { get; set; }
- bool IEquatable<ICacheable>.Equals(ICacheable? other) => Equals(other);
- void ICacheable.Evicted()
- {
- //Erase nonce
- Memory.UnsafeZeroMemory(RawNonce);
- }
-
- public override bool Equals(object? obj)
- {
- return obj is LoginClaim otherClaim && this.PublicKey!.Equals(otherClaim.PublicKey, StringComparison.Ordinal);
- }
- public override int GetHashCode() => PublicKey!.GetHashCode();
-
- void INonce.ComputeNonce(Span<byte> buffer)
- {
- RandomHash.GetRandomBytes(buffer);
- //Store copy
- RawNonce = buffer.ToArray();
- }
-
- bool INonce.VerifyNonce(ReadOnlySpan<byte> nonceBytes)
- {
- return CryptographicOperations.FixedTimeEquals(RawNonce.Span, nonceBytes);
- }
- }
-
- /*
- * Get method is invoked when the remote OAuth2 control has been passed back
- * to this server. If successfull 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<VfReturnType> GetAsync(HttpEntity entity)
- {
- //Make sure state and code parameters are available
- if (entity.QueryArgs.TryGetNonEmptyValue("state", out string? state) && entity.QueryArgs.TryGetNonEmptyValue("code", out string? code))
- {
- //Disable refer headers when nonce is set
- entity.Server.Headers["Referrer-Policy"] = "no-referrer";
-
- //Check for security navigation headers. This should be a browser redirect,
- if (!entity.Server.IsNavigation() || !entity.Server.IsUserInvoked())
- {
- //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 (ClaimStore.TryGetOrEvictRecord(state, out LoginClaim? claim) < 1)
- {
- entity.Redirect(RedirectType.Temporary, $"{Path}?result=expired");
- return VfReturnType.VirtualSkip;
- }
- //Lock on the claim to prevent replay
- lock (claim)
- {
- bool isValid = claim.VerifyNonce(state);
- //Evict the record inside the lock, also wipes nonce contents
- ClaimStore.EvictRecord(state);
-
- //Compare binary values of nonce incase of dicionary collision
- if (!isValid)
- {
- 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)
- {
- entity.Redirect(RedirectType.Temporary, $"{Path}?result=invalid");
- return VfReturnType.VirtualSkip;
- }
- //Store claim info
- token.PublicKey = claim.PublicKey;
- token.ClientId = claim.ClientId;
- //Generate the new nonce
- string nonce = token.ComputeNonce((int)Config.NonceByteSize);
- //Collect expired records
- AuthorizationStore.CollectRecords();
- //Register the access token
- AuthorizationStore.StoreRecord(nonce, token, Config.LoginNonceLifetime);
- //Prepare redirect
- entity.Redirect(RedirectType.Temporary, $"{Path}?result=authorized&nonce={nonce}");
- return VfReturnType.VirtualSkip;
- }
- //Check to see if there was an error code set
- if (entity.QueryArgs.TryGetNonEmptyValue("error", out string? errorCode))
- {
- 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<VfReturnType> PostAsync(HttpEntity entity)
- {
- ValErrWebMessage webm = new();
- //Get the finalization message
- using JsonDocument? request = await entity.GetJsonFromFileAsync();
- if (webm.Assert(request != null, "Request message is required"))
- {
- entity.CloseResponseJson(HttpStatusCode.BadRequest, webm);
- return VfReturnType.VirtualSkip;
- }
- //Recover the nonce
- string? base32Nonce = request.RootElement.GetPropString("nonce");
- if(webm.Assert(base32Nonce != null, message: "Nonce parameter is required"))
- {
- entity.CloseResponseJson(HttpStatusCode.UnprocessableEntity, webm);
- return VfReturnType.VirtualSkip;
- }
- //Validate nonce
- if (!NonceValidator.Validate(base32Nonce, webm))
- {
- entity.CloseResponseJson(HttpStatusCode.UnprocessableEntity, webm);
- return VfReturnType.VirtualSkip;
- }
- //Recover the access token
- if (AuthorizationStore.TryGetOrEvictRecord(base32Nonce!, out OAuthAccessState? token) < 1)
- {
- webm.Result = AUTH_ERROR_MESSAGE;
- entity.CloseResponse(webm);
- return VfReturnType.VirtualSkip;
- }
- bool valid;
- //Valid token, now verify the nonce within the locked context
- lock (token)
- {
- valid = token.VerifyNonce(base32Nonce);
- //Evict (wipes nonce)
- AuthorizationStore.EvictRecord(base32Nonce!);
- }
- if (webm.Assert(valid, AUTH_ERROR_MESSAGE))
- {
- entity.CloseResponse(webm);
- return VfReturnType.VirtualSkip;
- }
-
- //get the user's login information (ie userid)
- UserLoginData? userLogin = await GetLoginDataAsync(token, entity.EventCancellation);
-
- if(webm.Assert(userLogin?.UserId != null, AUTH_ERROR_MESSAGE))
- {
- entity.CloseResponse(webm);
- return VfReturnType.VirtualSkip;
- }
-
- //Fetch the user from the database
- IUser? user = await Config.Users.GetUserFromIDAsync(userLogin.UserId, entity.EventCancellation);
-
- if(user == null)
- {
- //Get the clients personal info to being login process
- AccountData? userAccount = await GetAccountDataAsync(token, entity.EventCancellation);
-
- if (webm.Assert(userAccount != null, AUTH_ERROR_MESSAGE))
- {
- entity.CloseResponse(webm);
- return VfReturnType.VirtualSkip;
- }
-
- //Validate the account data
- if (webm.Assert(AccountDataValidator.Validate(userAccount).IsValid, AUTH_ERROR_MESSAGE))
- {
- entity.CloseResponse(webm);
- return VfReturnType.VirtualSkip;
- }
-
- //make sure registration is enabled
- if (webm.Assert(Config.AllowRegistration, AUTH_ERROR_MESSAGE))
- {
- entity.CloseResponse(webm);
- return VfReturnType.VirtualSkip;
- }
- //Create new user, create random passwords
- byte[] randomPass = RandomHash.GetRandomBytes(Config.RandomPasswordSize);
- //Generate a new random passowrd incase the user wants to use a local account to log in sometime in the future
- PrivateString passhash = Config.Passwords.Hash(randomPass);
- //overwite the password bytes
- Memory.InitializeBlock(randomPass.AsSpan());
- try
- {
- //Create the user with the specified email address, minimum privilage level, and an empty password
- user = await Config.Users.CreateUserAsync(userLogin.UserId!, userAccount.EmailAddress, AccountManager.MINIMUM_LEVEL, passhash, entity.EventCancellation);
- //Set active status
- user.Status = UserStatus.Active;
- //Store the new profile
- user.SetProfile(userAccount);
- //Set the account creation origin
- 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";
- entity.CloseResponse(webm);
- return VfReturnType.VirtualSkip;
- }
- finally
- {
- passhash.Dispose();
- }
- }
- else
- {
- //Check for local only
- if (webm.Assert(!user.LocalOnly, AUTH_ERROR_MESSAGE))
- {
- entity.CloseResponse(webm);
- return VfReturnType.VirtualSkip;
- }
- //Make sure local accounts are allowed
- if (webm.Assert(!user.IsLocalAccount() || Config.AllowForLocalAccounts, AUTH_ERROR_MESSAGE))
- {
- entity.CloseResponse(webm);
- return VfReturnType.VirtualSkip;
- }
- //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))
- {
- entity.CloseResponse(webm);
- return VfReturnType.VirtualSkip;
- }
- }
- //Finalze login
- try
- {
- //Generate authoization
- webm.Token = entity.GenerateAuthorization(token.PublicKey!, token.ClientId!, user);
- //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 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;
- entity.InvalidateLogin();
- Log.Error(uue);
- }
- finally
- {
- user.Dispose();
- }
- entity.CloseResponse(webm);
- return VfReturnType.VirtualSkip;
- }
-
- /*
- * 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<VfReturnType> PutAsync(HttpEntity entity)
- {
- ValErrWebMessage webm = new();
-
- //Get the login message
- LoginClaim? claim = await entity.GetJsonFromFileAsync<LoginClaim>();
-
- if (webm.Assert(claim != null, "Emtpy message body"))
- {
- entity.CloseResponseJson(HttpStatusCode.BadRequest, webm);
- return VfReturnType.VirtualSkip;
- }
-
- //Validate the message
- if (!ClaimValidator.Validate(claim, webm))
- {
- entity.CloseResponseJson(HttpStatusCode.UnprocessableEntity, webm);
- return VfReturnType.VirtualSkip;
- }
-
- //Cleanup old records
- ClaimStore.CollectRecords();
- //Set nonce
- string base32Nonce = claim.ComputeNonce((int)Config.NonceByteSize);
- //build the redirect url
- webm.Result = BuildUrl(base32Nonce, claim.PublicKey!, entity.IsSecure ? "https" : "http", entity.Server.RequestUri.Authority, entity.Server.Encoding);
- //Store the claim
- ClaimStore.StoreRecord(base32Nonce, claim, Config.LoginNonceLifetime);
- webm.Success = true;
- //Response
- entity.CloseResponse(webm);
- return VfReturnType.VirtualSkip;
- }
-
- /*
- * 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
- */
-
- private string BuildUrl(string base32Nonce, string pubKey, ReadOnlySpan<char> scheme, ReadOnlySpan<char> redirectAuthority, Encoding enc)
- {
- //Char buffer for base32 and url building
- using UnsafeMemoryHandle<byte> buffer = Memory.UnsafeAlloc<byte>(8192, true);
- //get bin buffer slice
- Span<byte> binBuffer = buffer.Span[1024..];
-
- ReadOnlySpan<char> url;
- {
- //Get char buffer slice and cast to char
- Span<char> charBuf = MemoryMarshal.Cast<byte, char>(buffer.Span[..1024]);
- //buffer writer for easier syntax
- ForwardOnlyWriter<char> writer = new(charBuf);
- //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(redirectAuthority);
- writer.Append(Path);
- //url encode the redirect path and save it for later
- string redirectFiltered = Uri.EscapeDataString(writer.ToString());
- //reset the writer again to begin building the path
- writer.Reset();
- //Append the config redirect path
- writer.Append(Config.AccessCodeUrl.OriginalString);
- //begin query arguments
- writer.Append("&client_id=");
- writer.Append(Config.ClientID);
- //add the redirect url
- writer.Append("&redirect_uri=");
- writer.Append(redirectFiltered);
- //Append the state parameter
- writer.Append("&state=");
- writer.Append(base32Nonce);
- url = writer.AsSpan();
- }
- //Separate buffers
- Span<byte> encryptionBuffer = binBuffer[1024..];
- Span<byte> encodingBuffer = binBuffer[..1024];
- //Encode the url to binary
- int byteCount = enc.GetBytes(url, encodingBuffer);
- //Encrypt the binary
- ERRNO count = AccountManager.TryEncryptClientData(pubKey, encodingBuffer[..byteCount], in encryptionBuffer);
- //base64 encode the encrypted
- return Convert.ToBase64String(encryptionBuffer[0..(int)count]);
- }
- }
-}
diff --git a/VNLib.Plugins.Essentials.SocialOauth/UserLoginData.cs b/VNLib.Plugins.Essentials.SocialOauth/UserLoginData.cs
deleted file mode 100644
index cb2406c..0000000
--- a/VNLib.Plugins.Essentials.SocialOauth/UserLoginData.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
-* Copyright (c) 2022 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;
-
-#nullable enable
-
-namespace VNLib.Plugins.Essentials.SocialOauth
-{
- public class UserLoginData
- {
- [JsonPropertyName("user_id")]
- public string? UserId { get; set; }
- }
-}
diff --git a/VNLib.Plugins.Essentials.SocialOauth/VNLib.Plugins.Essentials.SocialOauth.csproj b/VNLib.Plugins.Essentials.SocialOauth/VNLib.Plugins.Essentials.SocialOauth.csproj
deleted file mode 100644
index 25abcb8..0000000
--- a/VNLib.Plugins.Essentials.SocialOauth/VNLib.Plugins.Essentials.SocialOauth.csproj
+++ /dev/null
@@ -1,54 +0,0 @@
-<Project Sdk="Microsoft.NET.Sdk">
-
- <PropertyGroup>
- <TargetFramework>net6.0</TargetFramework>
- <Authors>Vaughn Nugent</Authors>
- <Product>SocialOauth</Product>
- <Version>1.0.1.5</Version>
- <Copyright>Copyright © 2022 Vaughn Nugent</Copyright>
- <PackageProjectUrl>https://www.vaughnnugent.com/resources</PackageProjectUrl>
- <AssemblyName>SocialOauth</AssemblyName>
- <SignAssembly>True</SignAssembly>
- <AssemblyOriginatorKeyFile>\\vaughnnugent.com\Internal\Folder Redirection\vman\Documents\Programming\Software\StrongNameingKey.snk</AssemblyOriginatorKeyFile>
- </PropertyGroup>
-
- <ItemGroup>
- <PackageReference Include="ErrorProne.NET.CoreAnalyzers" Version="0.1.2">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
- </PackageReference>
- <PackageReference Include="ErrorProne.NET.Structs" Version="0.1.2">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
- </PackageReference>
- <PackageReference Include="FluentValidation" Version="11.3.0" />
- <PackageReference Include="RestSharp" Version="108.0.2" />
- </ItemGroup>
-
- <!-- Resolve nuget dll files and store them in the output dir -->
- <PropertyGroup>
- <!--Enable dynamic loading-->
- <EnableDynamicLoading>true</EnableDynamicLoading>
- <Nullable>enable</Nullable>
- <GenerateDocumentationFile>False</GenerateDocumentationFile>
- <AnalysisLevel>latest-all</AnalysisLevel>
- </PropertyGroup>
- <ItemGroup>
- <ProjectReference Include="..\..\..\VNLib\Essentials\src\VNLib.Plugins.Essentials.csproj" />
- <ProjectReference Include="..\..\Extensions\VNLib.Plugins.Extensions.Loading\VNLib.Plugins.Extensions.Loading.csproj" />
- <ProjectReference Include="..\..\Extensions\VNLib.Plugins.Extensions.Validation\VNLib.Plugins.Extensions.Validation.csproj" />
- <ProjectReference Include="..\..\PluginBase\VNLib.Plugins.PluginBase.csproj" />
- <ProjectReference Include="..\..\VNLib.Net.Rest.Client\VNLib.Net.Rest.Client.csproj" />
- </ItemGroup>
-
- <ItemGroup>
- <None Update="SocialOauth.json">
- <CopyToOutputDirectory>Always</CopyToOutputDirectory>
- </None>
- </ItemGroup>
-
- <Target Name="PostBuild" AfterTargets="PostBuildEvent">
- <Exec Command="start xcopy &quot;$(TargetDir)&quot; &quot;F:\Programming\Web Plugins\DevPlugins\$(TargetName)&quot; /E /Y /R" />
- </Target>
-
-</Project>
diff --git a/VNLib.Plugins.Essentials.SocialOauth/Validators/AccountDataValidator.cs b/VNLib.Plugins.Essentials.SocialOauth/Validators/AccountDataValidator.cs
deleted file mode 100644
index 7ebb37e..0000000
--- a/VNLib.Plugins.Essentials.SocialOauth/Validators/AccountDataValidator.cs
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
-* Copyright (c) 2022 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;
-
-#nullable enable
-
-namespace VNLib.Plugins.Essentials.SocialOauth.Validators
-{
- internal class AccountDataValidator : AbstractValidator<AccountData>
- {
- public AccountDataValidator() : base()
- {
- RuleFor(t => t.EmailAddress)
- .NotEmpty()
- .WithMessage("Your account does not have an email address assigned to it");
-
- RuleFor(t => t.EmailAddress)
- .EmailAddress()
- .WithMessage("Your account does not have a valid email address assigned to it");
-
- //Validate city
- RuleFor(t => t.City).MaximumLength(50);
- RuleFor(t => t.City).AlphaOnly();
-
- RuleFor(t => t.Company).MaximumLength(50);
- RuleFor(t => t.Company).SpecialCharacters();
-
- RuleFor(t => t.First).MaximumLength(35);
- RuleFor(t => t.First).AlphaOnly();
-
- RuleFor(t => t.Last).MaximumLength(35);
- RuleFor(t => t.Last).AlphaOnly();
-
- RuleFor(t => t.PhoneNumber)
- .EmptyPhoneNumber()
- .OverridePropertyName("Phone");
-
- //State must be 2 characters for us states if set
- RuleFor(t => t.State).Length(t => t.State?.Length != 0 ? 2 : 0);
-
- RuleFor(t => t.Street).MaximumLength(50);
- RuleFor(t => t.Street).AlphaNumericOnly();
-
- RuleFor(t => t.Zip).NumericOnly();
- //Allow empty zip codes, but if one is defined, is must be less than 7 characters
- RuleFor(t => t.Zip).Length(ad => ad.Zip?.Length != 0 ? 7 : 0);
- }
- }
-}
diff --git a/VNLib.Plugins.Essentials.SocialOauth/Validators/LoginMessageValidation.cs b/VNLib.Plugins.Essentials.SocialOauth/Validators/LoginMessageValidation.cs
deleted file mode 100644
index 86893c5..0000000
--- a/VNLib.Plugins.Essentials.SocialOauth/Validators/LoginMessageValidation.cs
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
-* Copyright (c) 2022 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<LoginMessage>
- {
- /*
- * A login message object is only used for common semantics within
- * the user-system so validation operations are different than a
- * normal login endpoint as named fields may be used differently
- */
- public LoginMessageValidation()
- {
- RuleFor(t => t.ClientID)
- .Length(10, 50)
- .WithMessage("Your browser is not sending required security information");
-
- RuleFor(t => t.ClientPublicKey)
- .NotEmpty()
- .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");
- RuleFor(t => t.LocalLanguage).AlphaNumericOnly();
- }
- }
-}