aboutsummaryrefslogtreecommitdiff
path: root/plugins/VNLib.Plugins.Essentials.SocialOauth/src
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/VNLib.Plugins.Essentials.SocialOauth/src')
-rw-r--r--plugins/VNLib.Plugins.Essentials.SocialOauth/src/ClientAccessTokenState.cs47
-rw-r--r--plugins/VNLib.Plugins.Essentials.SocialOauth/src/Endpoints/Auth0.cs38
-rw-r--r--plugins/VNLib.Plugins.Essentials.SocialOauth/src/Endpoints/DiscordOauth.cs33
-rw-r--r--plugins/VNLib.Plugins.Essentials.SocialOauth/src/Endpoints/GitHubOauth.cs38
-rw-r--r--plugins/VNLib.Plugins.Essentials.SocialOauth/src/OauthClientConfig.cs59
-rw-r--r--plugins/VNLib.Plugins.Essentials.SocialOauth/src/SocialEntryPoint.cs42
-rw-r--r--plugins/VNLib.Plugins.Essentials.SocialOauth/src/SocialOauthBase.cs499
-rw-r--r--plugins/VNLib.Plugins.Essentials.SocialOauth/src/Validators/LoginMessageValidation.cs15
8 files changed, 417 insertions, 354 deletions
diff --git a/plugins/VNLib.Plugins.Essentials.SocialOauth/src/ClientAccessTokenState.cs b/plugins/VNLib.Plugins.Essentials.SocialOauth/src/ClientAccessTokenState.cs
index 8a7aea3..18f4081 100644
--- a/plugins/VNLib.Plugins.Essentials.SocialOauth/src/ClientAccessTokenState.cs
+++ b/plugins/VNLib.Plugins.Essentials.SocialOauth/src/ClientAccessTokenState.cs
@@ -1,12 +1,12 @@
/*
-* Copyright (c) 2022 Vaughn Nugent
+* Copyright (c) 2023 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Essentials.SocialOauth
-* File: ClientAccessTokenState.cs
+* File: OAuthAccessState.cs
*
-* ClientAccessTokenState.cs is part of VNLib.Plugins.Essentials.SocialOauth which is part of the larger
-* VNLib collection of libraries and utilities.
+* 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
@@ -23,17 +23,11 @@
*/
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
+ public sealed class OAuthAccessState : IOAuthAccessState
{
///<inheritdoc/>
[JsonPropertyName("access_token")]
@@ -50,36 +44,5 @@ namespace VNLib.Plugins.Essentials.SocialOauth
///<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()
- {
- MemoryUtil.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/plugins/VNLib.Plugins.Essentials.SocialOauth/src/Endpoints/Auth0.cs b/plugins/VNLib.Plugins.Essentials.SocialOauth/src/Endpoints/Auth0.cs
index 3466ad0..2f99693 100644
--- a/plugins/VNLib.Plugins.Essentials.SocialOauth/src/Endpoints/Auth0.cs
+++ b/plugins/VNLib.Plugins.Essentials.SocialOauth/src/Endpoints/Auth0.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2022 Vaughn Nugent
+* Copyright (c) 2023 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Essentials.SocialOauth
@@ -24,7 +24,6 @@
using System;
using System.Linq;
-using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
@@ -32,13 +31,13 @@ using System.Collections.Generic;
using RestSharp;
-using VNLib.Net.Rest.Client;
using VNLib.Hashing;
using VNLib.Hashing.IdentityUtility;
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
{
@@ -46,13 +45,9 @@ namespace VNLib.Plugins.Essentials.SocialOauth.Endpoints
[ConfigurationName("auth0")]
internal sealed class Auth0 : SocialOauthBase
{
-
- protected override OauthClientConfig Config { get; }
-
-
private readonly Task<ReadOnlyJsonWebKey[]> Auth0VerificationJwk;
- public Auth0(PluginBase plugin, IReadOnlyDictionary<string, JsonElement> config) : base()
+ public Auth0(PluginBase plugin, IConfigScope config) : base(plugin, config)
{
string keyUrl = config["key_url"].GetString() ?? throw new KeyNotFoundException("Missing Auth0 'key_url' from config");
@@ -60,31 +55,6 @@ namespace VNLib.Plugins.Essentials.SocialOauth.Endpoints
//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.ObserveTask(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);
}
diff --git a/plugins/VNLib.Plugins.Essentials.SocialOauth/src/Endpoints/DiscordOauth.cs b/plugins/VNLib.Plugins.Essentials.SocialOauth/src/Endpoints/DiscordOauth.cs
index a701bdf..93cb22d 100644
--- a/plugins/VNLib.Plugins.Essentials.SocialOauth/src/Endpoints/DiscordOauth.cs
+++ b/plugins/VNLib.Plugins.Essentials.SocialOauth/src/Endpoints/DiscordOauth.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2022 Vaughn Nugent
+* Copyright (c) 2023 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Essentials.SocialOauth
@@ -25,9 +25,7 @@
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;
@@ -37,41 +35,14 @@ 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()
+ public DiscordOauth(PluginBase plugin, IConfigScope config) : base(plugin, config)
{
- Config = new("discord", config)
- {
- Passwords = plugin.GetPasswords(),
- Users = plugin.GetUserManager(),
- };
-
- InitPathAndLog(Config.EndpointPath, plugin.Log);
-
- //Load secrets
- _ = plugin.ObserveTask(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);
}
diff --git a/plugins/VNLib.Plugins.Essentials.SocialOauth/src/Endpoints/GitHubOauth.cs b/plugins/VNLib.Plugins.Essentials.SocialOauth/src/Endpoints/GitHubOauth.cs
index 7e8c576..3d6fa30 100644
--- a/plugins/VNLib.Plugins.Essentials.SocialOauth/src/Endpoints/GitHubOauth.cs
+++ b/plugins/VNLib.Plugins.Essentials.SocialOauth/src/Endpoints/GitHubOauth.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2022 Vaughn Nugent
+* Copyright (c) 2023 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Essentials.SocialOauth
@@ -23,9 +23,7 @@
*/
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;
@@ -37,7 +35,6 @@ 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
{
@@ -47,38 +44,11 @@ namespace VNLib.Plugins.Essentials.SocialOauth.Endpoints
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()
- {
-
+ 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");
-
- Config = new("github", config)
- {
- Passwords = plugin.GetPasswords(),
- Users = plugin.GetUserManager(),
- };
-
- InitPathAndLog(Config.EndpointPath, plugin.Log);
-
- //Load secrets
- _ = plugin.ObserveTask(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)
diff --git a/plugins/VNLib.Plugins.Essentials.SocialOauth/src/OauthClientConfig.cs b/plugins/VNLib.Plugins.Essentials.SocialOauth/src/OauthClientConfig.cs
index 9caf705..5b0d8ed 100644
--- a/plugins/VNLib.Plugins.Essentials.SocialOauth/src/OauthClientConfig.cs
+++ b/plugins/VNLib.Plugins.Essentials.SocialOauth/src/OauthClientConfig.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2022 Vaughn Nugent
+* Copyright (c) 2023 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Essentials.SocialOauth
@@ -23,31 +23,36 @@
*/
using System;
-using System.Text.Json;
+using System.Threading.Tasks;
using System.Collections.Generic;
using VNLib.Utils.Extensions;
using VNLib.Plugins.Essentials.Users;
using VNLib.Plugins.Essentials.Accounts;
-
+using VNLib.Plugins.Extensions.Loading;
+using VNLib.Plugins.Extensions.Loading.Users;
namespace VNLib.Plugins.Essentials.SocialOauth
{
- public sealed class OauthClientConfig
+ public sealed class OauthClientConfig : IAsyncConfigurable
{
+ private readonly string ConfigName;
+
- public OauthClientConfig(string configName, IReadOnlyDictionary<string, JsonElement> config)
+ public OauthClientConfig(PluginBase plugin, IConfigScope config)
{
- EndpointPath = config["path"].GetString() ?? throw new KeyNotFoundException($"Missing required key 'path' in config {configName}");
+ ConfigName = config.ScopeName;
+
+ 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}");
+ 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}");
+ 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);
@@ -58,12 +63,31 @@ namespace VNLib.Plugins.Essentials.SocialOauth
LoginNonceLifetime = config["valid_for_sec"].GetTimeSpan(TimeParseType.Seconds);
NonceByteSize = config["nonce_size"].GetUInt32();
RandomPasswordSize = config["password_size"].GetInt32();
+ InitClaimValidFor = config["claim_valid_for_sec"].GetTimeSpan(TimeParseType.Seconds);
+
+ Users = plugin.GetOrCreateSingleton<UserManager>();
+ Passwords = plugin.GetPasswords();
}
-
- public string ClientID { get; set; } = string.Empty;
+ public async Task ConfigureServiceAsync(PluginBase plugin)
+ {
+ //Get id/secret
+ Task<SecretResult?> clientIdTask = plugin.TryGetSecretAsync($"{ConfigName}_client_id");
+ Task<SecretResult?> secretTask = plugin.TryGetSecretAsync($"{ConfigName}_client_secret");
+
+ await Task.WhenAll(secretTask, clientIdTask);
+
+ using SecretResult? secret = await secretTask;
+ using SecretResult? clientId = await clientIdTask;
+
+ ClientID = clientId?.Result.ToString() ?? throw new KeyNotFoundException($"Missing {ConfigName} client id from config or vault");
+ ClientSecret = secret?.Result.ToString() ?? throw new KeyNotFoundException($"Missing the {ConfigName} client secret from config or vault");
+ }
+
+
+ public string ClientID { get; private set; } = string.Empty;
- public string ClientSecret { get; set; } = string.Empty;
+ public string ClientSecret { get; private set; } = string.Empty;
/// <summary>
@@ -92,9 +116,9 @@ namespace VNLib.Plugins.Essentials.SocialOauth
/// <summary>
/// The user store to create/get users from
/// </summary>
- public IUserManager Users { get; init; }
+ public IUserManager Users { get; }
- public PasswordHashing Passwords { get; init; }
+ public IPasswordHashingProvider Passwords { get; }
/// <summary>
/// The endpoint route/path
@@ -122,5 +146,10 @@ namespace VNLib.Plugins.Essentials.SocialOauth
/// The size (in bytes) of the random password generated for new users
/// </summary>
public int RandomPasswordSize { get; }
+
+ /// <summary>
+ /// The initial time the login claim is valid for
+ /// </summary>
+ public TimeSpan InitClaimValidFor { get; }
}
}
diff --git a/plugins/VNLib.Plugins.Essentials.SocialOauth/src/SocialEntryPoint.cs b/plugins/VNLib.Plugins.Essentials.SocialOauth/src/SocialEntryPoint.cs
index d0f7a84..05152b2 100644
--- a/plugins/VNLib.Plugins.Essentials.SocialOauth/src/SocialEntryPoint.cs
+++ b/plugins/VNLib.Plugins.Essentials.SocialOauth/src/SocialEntryPoint.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2022 Vaughn Nugent
+* Copyright (c) 2023 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Essentials.SocialOauth
@@ -23,7 +23,6 @@
*/
using System;
-using System.Collections.Generic;
using VNLib.Utils.Logging;
using VNLib.Plugins.Essentials.SocialOauth.Endpoints;
@@ -39,32 +38,25 @@ namespace VNLib.Plugins.Essentials.SocialOauth
protected override void OnLoad()
{
- try
+ //Get the discord oauth config from the config file
+ if (this.HasConfigForType<DiscordOauth>())
{
- //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");
- }
+ //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");
}
- catch(KeyNotFoundException kne)
+
+ if (this.HasConfigForType<Auth0>())
{
- Log.Error("Missing required configuration variables, {reason}", kne.Message);
+ //Add the auth0 login endpoint
+ this.Route<Auth0>();
+ Log.Information("Auth0 social OAuth authentication loaded");
}
}
diff --git a/plugins/VNLib.Plugins.Essentials.SocialOauth/src/SocialOauthBase.cs b/plugins/VNLib.Plugins.Essentials.SocialOauth/src/SocialOauthBase.cs
index 73c2ab5..2ad3a8e 100644
--- a/plugins/VNLib.Plugins.Essentials.SocialOauth/src/SocialOauthBase.cs
+++ b/plugins/VNLib.Plugins.Essentials.SocialOauth/src/SocialOauthBase.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2022 Vaughn Nugent
+* Copyright (c) 2023 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Essentials.SocialOauth
@@ -25,6 +25,7 @@
using System;
using System.Net;
using System.Text;
+using System.Buffers;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
@@ -32,6 +33,7 @@ using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text.Json.Serialization;
using System.Runtime.InteropServices;
+using System.Diagnostics.CodeAnalysis;
using FluentValidation;
@@ -39,17 +41,18 @@ using RestSharp;
using VNLib.Net.Http;
using VNLib.Net.Rest.Client;
using VNLib.Hashing;
+using VNLib.Hashing.IdentityUtility;
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;
+using VNLib.Plugins.Extensions.Loading;
+using VNLib.Plugins.Extensions.Validation;
namespace VNLib.Plugins.Essentials.SocialOauth
{
@@ -62,11 +65,17 @@ namespace VNLib.Plugins.Essentials.SocialOauth
const string AUTH_ERROR_MESSAGE = "You have no pending authentication requests.";
const string AUTH_GRANT_SESSION_NAME = "auth";
+ const string SESSION_SIG_KEY_NAME = "soa.sig";
+ const string SESSION_TOKEN_KEY_NAME = "soa.tkn";
+ const string CLAIM_COOKIE_NAME = "extern-claim";
+ const int SIGNING_KEY_SIZE = 32;
+
+ private static HMAC GetSigningAlg(byte[] key) => new HMACSHA256(key);
/// <summary>
/// The client configuration struct passed during base class construction
/// </summary>
- protected abstract OauthClientConfig Config { get; }
+ protected virtual OauthClientConfig Config { get; }
///<inheritdoc/>
protected override ProtectionSettings EndpointProtectionSettings { get; } = new()
@@ -82,17 +91,13 @@ namespace VNLib.Plugins.Essentials.SocialOauth
/// 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()
+ protected SocialOauthBase(PluginBase plugin, IConfigScope config)
{
- ClaimStore = new(StringComparer.OrdinalIgnoreCase);
- AuthorizationStore = new(StringComparer.OrdinalIgnoreCase);
ClaimValidator = GetClaimValidator();
NonceValidator = GetNonceValidator();
AccountDataValidator = new AccountDataValidator();
@@ -108,6 +113,12 @@ namespace VNLib.Plugins.Essentials.SocialOauth
//Configure rest client to comunications to main discord api
ClientPool = new(10, poolOptions, StaticClientPoolInitializer);
+
+ //Get the configuration element for the derrived type
+ Config = plugin.CreateService<OauthClientConfig>(config);
+
+ //Init endpoint
+ InitPathAndLog(Config.EndpointPath, plugin.Log);
}
private static IValidator<LoginClaim> GetClaimValidator()
@@ -133,6 +144,7 @@ namespace VNLib.Plugins.Essentials.SocialOauth
return val;
}
+ ///<inheritdoc/>
protected override ERRNO PreProccess(HttpEntity entity)
{
if (!base.PreProccess(entity))
@@ -150,7 +162,7 @@ namespace VNLib.Plugins.Essentials.SocialOauth
}
//Make sure the user is not logged in
- return !(entity.LoginCookieMatches() || entity.TokenMatches());
+ return !entity.IsClientAuthorized(AuthorzationCheckLevel.Any);
}
/// <summary>
@@ -176,8 +188,7 @@ namespace VNLib.Plugins.Essentials.SocialOauth
/// 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)
+ protected virtual 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);
@@ -207,7 +218,6 @@ namespace VNLib.Plugins.Essentials.SocialOauth
/// <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)
@@ -217,45 +227,94 @@ namespace VNLib.Plugins.Essentials.SocialOauth
/// <returns></returns>
protected abstract Task<UserLoginData?> GetLoginDataAsync(IOAuthAccessState clientAccess, CancellationToken cancellation);
- sealed class LoginClaim : ICacheable, INonce
+ sealed class LoginClaim : IClientSecInfo
{
[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
- MemoryUtil.UnsafeZeroMemory(RawNonce);
- }
+ [JsonPropertyName("exp")]
+ public long ExpirationSeconds { get; set; }
- public override bool Equals(object? obj)
+ [JsonPropertyName("iat")]
+ public long IssuedAtTime { get; set; }
+
+ [JsonPropertyName("nonce")]
+ public string? Nonce { get; set; }
+
+ public void ComputeNonce(int nonceSize)
{
- return obj is LoginClaim otherClaim && this.PublicKey!.Equals(otherClaim.PublicKey, StringComparison.Ordinal);
+ byte[] buffer = ArrayPool<byte>.Shared.Rent(nonceSize);
+ try
+ {
+ Span<byte> nonce = buffer.AsSpan(0, nonceSize);
+
+ //get random data
+ RandomHash.GetRandomBytes(nonce);
+
+ //Encode nonce
+ Nonce = VnEncoding.ToBase32String(nonce);
+ }
+ finally
+ {
+ MemoryUtil.InitializeBlock(buffer.AsSpan());
+ ArrayPool<byte>.Shared.Return(buffer);
+ }
}
- public override int GetHashCode() => PublicKey!.GetHashCode();
+ }
+
+ /*
+ * 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();
- void INonce.ComputeNonce(Span<byte> buffer)
+ //Get the login message
+ LoginClaim? claim = await entity.GetJsonFromFileAsync<LoginClaim>();
+
+ if (webm.Assert(claim != null, "Emtpy message body"))
{
- RandomHash.GetRandomBytes(buffer);
- //Store copy
- RawNonce = buffer.ToArray();
+ entity.CloseResponseJson(HttpStatusCode.BadRequest, webm);
+ return VfReturnType.VirtualSkip;
}
- bool INonce.VerifyNonce(ReadOnlySpan<byte> nonceBytes)
+ //Validate the message
+ if (!ClaimValidator.Validate(claim, webm))
{
- return CryptographicOperations.FixedTimeEquals(RawNonce.Span, nonceBytes);
+ 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()
+ .WithEncoding(entity.Server.Encoding)
+ .WithUrl(entity.IsSecure ? "https" : "http", entity.Server.RequestUri.Authority, Path)
+ .WithNonce(claim.Nonce!)
+ .Build(Config)
+ .Encrypt(entity, claim);
+
+ //Sign and set the claim cookie
+ SignAndSetCookie(entity, claim);
+
+ webm.Success = true;
+ //Response
+ entity.CloseResponse(webm);
+ return VfReturnType.VirtualSkip;
}
/*
@@ -272,65 +331,61 @@ namespace VNLib.Plugins.Essentials.SocialOauth
{
//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())
{
+ 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 (ClaimStore.TryGetOrEvictRecord(state, out LoginClaim? claim) < 1)
+ if (!VerifyAndGetClaim(entity, out LoginClaim? claim))
{
+ ClearClaimData(entity);
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;
- }
+ //Confirm the nonce matches the claim
+ if (string.CompareOrdinal(claim.Nonce, state) != 0)
+ {
+ 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)
+ if (token == null)
{
+ ClearClaimData(entity);
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);
+
+ //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
+ SignAndSetCookie(entity, claim);
+
//Prepare redirect
- entity.Redirect(RedirectType.Temporary, $"{Path}?result=authorized&nonce={nonce}");
+ 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))
{
+ 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;
@@ -358,6 +413,7 @@ namespace VNLib.Plugins.Essentials.SocialOauth
//Recover the nonce
string? base32Nonce = request.RootElement.GetPropString("nonce");
+
if(webm.Assert(base32Nonce != null, message: "Nonce parameter is required"))
{
entity.CloseResponseJson(HttpStatusCode.UnprocessableEntity, webm);
@@ -370,29 +426,30 @@ namespace VNLib.Plugins.Essentials.SocialOauth
entity.CloseResponseJson(HttpStatusCode.UnprocessableEntity, webm);
return VfReturnType.VirtualSkip;
}
-
+
//Recover the access token
- if (AuthorizationStore.TryGetOrEvictRecord(base32Nonce!, out OAuthAccessState? token) < 1)
+ bool cookieValid = VerifyAndGetClaim(entity, out LoginClaim? claim);
+
+ if (webm.Assert(cookieValid, AUTH_ERROR_MESSAGE))
{
- 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))
+
+ //We can clear the client's access claim
+ 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))
{
entity.CloseResponse(webm);
return VfReturnType.VirtualSkip;
- }
+ }
+
+ //Safe to recover the access token
+ IOAuthAccessState token = entity.Session.GetObject<OAuthAccessState>(SESSION_TOKEN_KEY_NAME);
//get the user's login information (ie userid)
UserLoginData? userLogin = await GetLoginDataAsync(token, entity.EventCancellation);
@@ -430,12 +487,9 @@ namespace VNLib.Plugins.Essentials.SocialOauth
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
- MemoryUtil.InitializeBlock(randomPass.AsSpan());
+ using PrivateString passhash = Config.Passwords.GetRandomPassword(Config.RandomPasswordSize);
try
{
//Create the user with the specified email address, minimum privilage level, and an empty password
@@ -454,10 +508,6 @@ namespace VNLib.Plugins.Essentials.SocialOauth
entity.CloseResponse(webm);
return VfReturnType.VirtualSkip;
}
- finally
- {
- passhash.Dispose();
- }
}
else
{
@@ -467,12 +517,14 @@ namespace VNLib.Plugins.Essentials.SocialOauth
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)
{
@@ -490,14 +542,17 @@ namespace VNLib.Plugins.Essentials.SocialOauth
try
{
//Generate authoization
- webm.Token = entity.GenerateAuthorization(token.PublicKey!, token.ClientId!, user);
+ 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
@@ -520,7 +575,10 @@ namespace VNLib.Plugins.Essentials.SocialOauth
webm.Token = null;
webm.Result = AUTH_ERROR_MESSAGE;
webm.Success = false;
+
+ //destroy any login data on failure
entity.InvalidateLogin();
+
Log.Error(uue);
}
finally
@@ -530,52 +588,10 @@ namespace VNLib.Plugins.Essentials.SocialOauth
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
@@ -584,52 +600,199 @@ namespace VNLib.Plugins.Essentials.SocialOauth
* 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)
+ sealed class LoginUriBuilder
{
- //Char buffer for base32 and url building
- using UnsafeMemoryHandle<byte> buffer = MemoryUtil.UnsafeAlloc<byte>(8192, true);
- //get bin buffer slice
- Span<byte> binBuffer = buffer.Span[1024..];
- Span<byte> charBuffer = buffer.Span[..1024];
-
- ReadOnlySpan<char> url;
+ private readonly IMemoryHandle<byte> _buffer;
+
+ private Span<byte> _binBuffer => _buffer.Span[1024..];
+ private Span<char> _charBuffer => MemoryMarshal.Cast<byte, char>(_buffer.Span[..1024]);
+
+ private string? redirectUrl;
+ private string? nonce;
+ private Encoding _encoding;
+
+ private int _urlCharPointer;
+
+ public LoginUriBuilder()
+ {
+ //Alloc buffer
+ _buffer = MemoryUtil.SafeAllocNearestPage<byte>(8000, true);
+
+ //Set default encoding
+ _encoding = Encoding.UTF8;
+ }
+
+ public LoginUriBuilder WithUrl(ReadOnlySpan<char> scheme, ReadOnlySpan<char> authority, ReadOnlySpan<char> path)
{
- //Get char buffer slice and cast to char
- Span<char> charBuf = MemoryMarshal.Cast<byte, char>(charBuffer);
//buffer writer for easier syntax
- ForwardOnlyWriter<char> writer = new(charBuf);
+ ForwardOnlyWriter<char> writer = new(_charBuffer);
//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);
+ writer.Append(authority);
+ 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();
+ 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 LoginUriBuilder Build(OauthClientConfig config)
+ {
+ //buffer writer for easier syntax
+ ForwardOnlyWriter<char> writer = new(_charBuffer);
+
//Append the config redirect path
- writer.Append(Config.AccessCodeUrl.OriginalString);
+ writer.Append(config.AccessCodeUrl.OriginalString);
//begin query arguments
writer.Append("&client_id=");
- writer.Append(Config.ClientID);
+ writer.Append(config.ClientID);
//add the redirect url
writer.Append("&redirect_uri=");
- writer.Append(redirectFiltered);
+ writer.Append(redirectUrl);
//Append the state parameter
writer.Append("&state=");
- writer.Append(base32Nonce);
- url = writer.AsSpan();
+ writer.Append(nonce);
+
+ //Update url pointer
+ _urlCharPointer = writer.Written;
+
+ return this;
}
- //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 = AccountUtil.TryEncryptClientData(pubKey, encodingBuffer[..byteCount], in encryptionBuffer);
- //base64 encode the encrypted
- return Convert.ToBase64String(encryptionBuffer[0..(int)count]);
+
+ public string Encrypt(HttpEntity client, IClientSecInfo secInfo)
+ {
+ try
+ {
+ ReadOnlySpan<char> url = _charBuffer[.._urlCharPointer];
+
+ //Separate buffers
+ Span<byte> encryptionBuffer = _binBuffer[1024..];
+ Span<byte> 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]);
+ }
+ finally
+ {
+ _urlCharPointer = 0;
+ //Dispose buffer
+ _buffer.Dispose();
+ }
+ }
+
+ }
+
+ private static bool VerifyAndGetClaim(HttpEntity entity, [NotNullWhen(true)] out LoginClaim? claim)
+ {
+ claim = null;
+
+ //Try to get the cookie
+ if(!entity.Server.GetCookie(CLAIM_COOKIE_NAME, out string? cookieValue))
+ {
+ return false;
+ }
+
+ //Recover the signing key from the user's session
+ string sigKey = entity.Session[SESSION_SIG_KEY_NAME];
+ byte[]? key = VnEncoding.FromBase32String(sigKey);
+
+ if (key == null)
+ {
+ return false;
+ }
+
+ try
+ {
+ //Try to parse the jwt
+ using JsonWebToken jwt = JsonWebToken.Parse(cookieValue);
+
+ //Verify the jwt
+ using(HMAC alg = GetSigningAlg(key))
+ {
+ if (!jwt.Verify(alg))
+ {
+ return false;
+ }
+ }
+
+ //Recover the clam from the jwt
+ claim = jwt.GetPayload<LoginClaim>();
+
+ //Verify the expiration time
+ return claim.ExpirationSeconds > entity.RequestedTimeUtc.ToUnixTimeSeconds();
+ }
+ catch (FormatException)
+ {
+ return false;
+ }
+ finally
+ {
+ MemoryUtil.InitializeBlock(key.AsSpan());
+ }
+ }
+
+ private static void ClearClaimData(HttpEntity entity)
+ {
+ if (entity.Server.RequestCookies.ContainsKey(CLAIM_COOKIE_NAME))
+ {
+ entity.Server.ExpireCookie(CLAIM_COOKIE_NAME);
+ }
+
+ entity.Session[SESSION_SIG_KEY_NAME] = null!;
+ }
+
+ private void SignAndSetCookie(HttpEntity entity, LoginClaim claim)
+ {
+ //Setup Jwt
+ using JsonWebToken jwt = new();
+
+ //Write claim body
+ jwt.WritePayload(claim);
+
+ //Generate signing key
+ byte[] sigKey = RandomHash.GetRandomBytes(SIGNING_KEY_SIZE);
+
+ //Sign the jwt
+ using(HMAC alg = GetSigningAlg(sigKey))
+ {
+ jwt.Sign(alg);
+ }
+
+ //Build and set cookie
+ HttpCookie cookie = new(CLAIM_COOKIE_NAME, jwt.Compile())
+ {
+ Secure = true,
+ HttpOnly = true,
+ ValidFor = Config.InitClaimValidFor,
+ SameSite = CookieSameSite.SameSite
+ };
+
+ entity.Server.SetCookie(in cookie);
+
+ //Encode and store the signing key in the clien't session
+ entity.Session[SESSION_SIG_KEY_NAME] = VnEncoding.ToBase32String(sigKey);
+
+ MemoryUtil.InitializeBlock(sigKey.AsSpan());
}
}
}
diff --git a/plugins/VNLib.Plugins.Essentials.SocialOauth/src/Validators/LoginMessageValidation.cs b/plugins/VNLib.Plugins.Essentials.SocialOauth/src/Validators/LoginMessageValidation.cs
index 86893c5..3cf4e70 100644
--- a/plugins/VNLib.Plugins.Essentials.SocialOauth/src/Validators/LoginMessageValidation.cs
+++ b/plugins/VNLib.Plugins.Essentials.SocialOauth/src/Validators/LoginMessageValidation.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2022 Vaughn Nugent
+* Copyright (c) 2023 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Essentials.SocialOauth
@@ -40,12 +40,16 @@ namespace VNLib.Plugins.Essentials.SocialOauth.Validators
*/
public LoginMessageValidation()
{
- RuleFor(t => t.ClientID)
+ RuleFor(t => t.ClientId)
.Length(10, 50)
- .WithMessage("Your browser is not sending required security information");
+ .WithMessage("Your browser is not sending required security information")
+ .IllegalCharacters()
+ .WithMessage("Your browser is not sending required security information");
RuleFor(t => t.ClientPublicKey)
- .NotEmpty()
+ .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
@@ -53,8 +57,9 @@ namespace VNLib.Plugins.Essentials.SocialOauth.Validators
RuleFor(t => t.LocalLanguage)
.NotEmpty()
+ .WithMessage("Your language is not supported")
+ .AlphaNumericOnly()
.WithMessage("Your language is not supported");
- RuleFor(t => t.LocalLanguage).AlphaNumericOnly();
}
}
}