diff options
Diffstat (limited to 'plugins/VNLib.Plugins.Essentials.Auth.Social/src')
9 files changed, 217 insertions, 33 deletions
diff --git a/plugins/VNLib.Plugins.Essentials.Auth.Social/src/ClientClaimManager.cs b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/ClientClaimManager.cs index d078964..033f577 100644 --- a/plugins/VNLib.Plugins.Essentials.Auth.Social/src/ClientClaimManager.cs +++ b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/ClientClaimManager.cs @@ -120,7 +120,7 @@ namespace VNLib.Plugins.Essentials.Auth.Social Cookies.SetCookie(entity, jwt.Compile()); //Encode and store the signing key in the clien't session - entity.Session[SESSION_SIG_KEY_NAME] = VnEncoding.ToBase64UrlSafeString(sigKey, false); + entity.Session[SESSION_SIG_KEY_NAME] = VnEncoding.Base64UrlEncode(sigKey, false); //Clear the signing key MemoryUtil.InitializeBlock(sigKey); diff --git a/plugins/VNLib.Plugins.Essentials.Auth.Social/src/LoginClaim.cs b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/LoginClaim.cs index 30a51fa..5d1b2dc 100644 --- a/plugins/VNLib.Plugins.Essentials.Auth.Social/src/LoginClaim.cs +++ b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/LoginClaim.cs @@ -65,11 +65,11 @@ namespace VNLib.Plugins.Essentials.Auth.Social RandomHash.GetRandomBytes(buffer.Span); //Base32-Encode nonce and save it - Nonce = VnEncoding.ToBase64UrlSafeString(buffer.Span, false); + Nonce = VnEncoding.Base64UrlEncode(buffer.Span, includePadding: false); } finally { - MemoryUtil.InitializeBlock(buffer.Span); + MemoryUtil.InitializeBlock(ref buffer.GetReference(), buffer.IntLength); } } } diff --git a/plugins/VNLib.Plugins.Essentials.Auth.Social/src/LoginUriBuilder.cs b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/LoginUriBuilder.cs index 4ed6ffd..fc94d5a 100644 --- a/plugins/VNLib.Plugins.Essentials.Auth.Social/src/LoginUriBuilder.cs +++ b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/LoginUriBuilder.cs @@ -96,7 +96,7 @@ namespace VNLib.Plugins.Essentials.Auth.Social ForwardOnlyWriter<char> writer = new(charBuffer); //Append the config redirect path - writer.Append(Config.AccessCodeUrl.OriginalString); + writer.Append(Config.AuthorizationUrl.OriginalString); //begin query arguments writer.AppendSmall("&client_id="); writer.Append(Config.ClientID.Value); diff --git a/plugins/VNLib.Plugins.Essentials.Auth.Social/src/OAuthSiteAdapter.cs b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/OAuthSiteAdapter.cs index 37dd7e0..ef90185 100644 --- a/plugins/VNLib.Plugins.Essentials.Auth.Social/src/OAuthSiteAdapter.cs +++ b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/OAuthSiteAdapter.cs @@ -22,6 +22,7 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ +using System; using System.Net; using System.Text; using System.Threading; @@ -49,7 +50,7 @@ namespace VNLib.Plugins.Essentials.Auth.Social { RestClientOptions poolOptions = new() { - MaxTimeout = 5000, + Timeout = TimeSpan.FromSeconds(5), AutomaticDecompression = DecompressionMethods.All, Encoding = Encoding.UTF8, //disable redirects, api should not redirect diff --git a/plugins/VNLib.Plugins.Essentials.Auth.Social/src/OauthClientConfig.cs b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/OauthClientConfig.cs index a3b43ad..7efd4df 100644 --- a/plugins/VNLib.Plugins.Essentials.Auth.Social/src/OauthClientConfig.cs +++ b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/OauthClientConfig.cs @@ -29,6 +29,8 @@ using System.Collections.Generic; using VNLib.Utils.Logging; using VNLib.Utils.Extensions; using VNLib.Plugins.Extensions.Loading; +using VNLib.Plugins.Essentials.Auth.Social.openid; +using VNLib.Plugins.Extensions.Loading.Configuration; namespace VNLib.Plugins.Essentials.Auth.Social { @@ -42,25 +44,25 @@ namespace VNLib.Plugins.Essentials.Auth.Social public OauthClientConfig(PluginBase plugin, IConfigScope config) { - EndpointPath = config["path"].GetString() ?? throw new KeyNotFoundException($"Missing required key 'path' in config {config.ScopeName}"); - - //Set discord account origin - AccountOrigin = config["account_origin"].GetString() ?? throw new KeyNotFoundException($"Missing required key 'account_origin' in config {config.ScopeName}"); - - //Get the auth and token urls - string authUrl = config["authorization_url"].GetString() ?? throw new KeyNotFoundException($"Missing required key 'authorization_url' in config {config.ScopeName}"); - string tokenUrl = config["token_url"].GetString() ?? throw new KeyNotFoundException($"Missing required key 'token_url' in config {config.ScopeName}"); - string userUrl = config["user_data_url"].GetString() ?? throw new KeyNotFoundException($"Missing required key 'user_data_url' in config {config.ScopeName}"); + EndpointPath = config.GetRequiredProperty("path", p => p.GetString()!); + AccountOrigin = config.GetRequiredProperty("account_origin", p => p.GetString()!); + + OpenIdPortalConfig portalConf = config.Deserialze<OpenIdPortalConfig>()!; + + Validate.NotNull(portalConf.AuthorizationEndpoint, $"Missing authorization endpoint for {config.ScopeName}"); + Validate.NotNull(portalConf.TokenEndpoint, $"Missing token endpoint for {config.ScopeName}"); + Validate.NotNull(portalConf.UserDataEndpoint, $"Missing user-data endpoint for {config.ScopeName}"); + //Create the uris - AccessCodeUrl = new(authUrl); - AccessTokenUrl = new(tokenUrl); - UserDataUrl = new(userUrl); + AuthorizationUrl = new(portalConf.AuthorizationEndpoint); + AccessTokenUrl = new(portalConf.TokenEndpoint); + UserDataUrl = new(portalConf.UserDataEndpoint); - AllowForLocalAccounts = config["allow_for_local"].GetBoolean(); - AllowRegistration = config["allow_registration"].GetBoolean(); - NonceByteSize = config["nonce_size"].GetUInt32(); - RandomPasswordSize = config["password_size"].GetInt32(); - InitClaimValidFor = config["claim_valid_for_sec"].GetTimeSpan(TimeParseType.Seconds); + AllowForLocalAccounts = config.GetValueOrDefault("allow_for_local", p => p.GetBoolean(), false); + AllowRegistration = config.GetValueOrDefault("allow_registration", p => p.GetBoolean(), false); + NonceByteSize = config.GetRequiredProperty("nonce_size", p => p.GetUInt32()); + RandomPasswordSize = config.GetRequiredProperty("password_size", p => p.GetInt32()); + InitClaimValidFor = config.GetRequiredProperty("claim_valid_for_sec", p => p.GetTimeSpan(TimeParseType.Seconds)); //Setup async lazy loaders for secrets ClientID = plugin.GetSecretAsync($"{config.ScopeName}_client_id") @@ -101,7 +103,7 @@ namespace VNLib.Plugins.Essentials.Auth.Social /// The URL to redirect the user to the OAuth2 service /// to begin the authentication process /// </summary> - public Uri AccessCodeUrl { get; } + public Uri AuthorizationUrl { get; } /// <summary> /// The remote endoint to exchange codes for access tokens diff --git a/plugins/VNLib.Plugins.Essentials.Auth.Social/src/SocialOauthBase.cs b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/SocialOauthBase.cs index 91bf147..f381fb8 100644 --- a/plugins/VNLib.Plugins.Essentials.Auth.Social/src/SocialOauthBase.cs +++ b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/SocialOauthBase.cs @@ -63,7 +63,11 @@ namespace VNLib.Plugins.Essentials.Auth.Social const string AUTH_GRANT_SESSION_NAME = "auth"; const string SESSION_TOKEN_KEY_NAME = "soa.tkn"; const string CLAIM_COOKIE_NAME = "extern-claim"; - + + private static readonly IValidator<LoginClaim> ClaimValidator = GetClaimValidator(); + private static readonly IValidator<string> NonceValidator = GetNonceValidator(); + private static readonly AccountDataValidator AccountDataValidator = new (); + /// <summary> /// The client configuration struct passed during base class construction @@ -81,19 +85,13 @@ namespace VNLib.Plugins.Essentials.Auth.Social /// <summary> /// The user manager used to create and manage user accounts /// </summary> - protected IUserManager Users { get; } - - private readonly IValidator<LoginClaim> ClaimValidator; - private readonly IValidator<string> NonceValidator; - private readonly IValidator<AccountData> AccountDataValidator; + protected IUserManager Users { get; } + + private readonly ClientClaimManager _claims; protected SocialOauthBase(PluginBase plugin, IConfigScope config) { - ClaimValidator = GetClaimValidator(); - NonceValidator = GetNonceValidator(); - AccountDataValidator = new AccountDataValidator(); - //Get the configuration element for the derrived type Config = plugin.CreateService<OauthClientConfig>(config); diff --git a/plugins/VNLib.Plugins.Essentials.Auth.Social/src/openid/OpenIdPortalConfig.cs b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/openid/OpenIdPortalConfig.cs new file mode 100644 index 0000000..97736ab --- /dev/null +++ b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/openid/OpenIdPortalConfig.cs @@ -0,0 +1,46 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials.Auth.Social +* File: OpenIdPortalConfig.cs +* +* OpenIdPortalConfig.cs is part of VNLib.Plugins.Essentials.Auth.Social which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials.Auth.Social 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.Auth.Social is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System.Text.Json.Serialization; + +namespace VNLib.Plugins.Essentials.Auth.Social.openid +{ + public sealed class OpenIdPortalConfig + { + [JsonPropertyName("issuer")] + public string IssuerUrl { get; set; } + + [JsonPropertyName("authorization_endpoint")] + public string AuthorizationEndpoint { get; set; } + + [JsonPropertyName("token_endpoint")] + public string TokenEndpoint { get; set; } + + [JsonPropertyName("userinfo_endpoint")] + public string UserDataEndpoint { get; set; } + + [JsonPropertyName("jwks_uri")] + public string KeysEndpoint { get; set; } + } +} diff --git a/plugins/VNLib.Plugins.Essentials.Auth.Social/src/openid/OpenIdProviderValidator.cs b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/openid/OpenIdProviderValidator.cs new file mode 100644 index 0000000..bde0a88 --- /dev/null +++ b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/openid/OpenIdProviderValidator.cs @@ -0,0 +1,70 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials.Auth.Social +* File: OauthClientConfig.cs +* +* OauthClientConfig.cs is part of VNLib.Plugins.Essentials.Auth.Social which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials.Auth.Social 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.Auth.Social 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; + +namespace VNLib.Plugins.Essentials.Auth.Social.openid +{ + public sealed class OpenIdProviderValidator : AbstractValidator<OpenIdPortalConfig> + { + public OpenIdProviderValidator(string discoveryUrl) + { + /* + * Discovery url will be compared to make sure the + * host is on the same domain as the issuer + */ + Uri discUrl = new(discoveryUrl); + + RuleFor(i => i.IssuerUrl) + .Matches($"^{discUrl.Scheme}://{discUrl.Host}") + .WithMessage("Issuer must be on the same domain as the discovery url"); + + RuleFor(i => i.AuthorizationEndpoint) + .NotEmpty() + .WithMessage("Authorization endpoint is required") + .Matches($"^{discUrl.Scheme}://{discUrl.Host}") + .WithMessage("Authorization endpoint must be on the same domain as the discovery url"); + + RuleFor(i => i.TokenEndpoint) + .NotEmpty() + .WithMessage("Token endpoint is required") + .Matches($"^{discUrl.Scheme}://{discUrl.Host}") + .WithMessage("Token endpoint must be on the same domain as the discovery url"); + + RuleFor(i => i.UserDataEndpoint) + .NotEmpty() + .WithMessage("User data endpoint is required") + .Matches($"^{discUrl.Scheme}://{discUrl.Host}") + .WithMessage("User data endpoint must be on the same domain as the discovery url"); + + RuleFor(i => i.KeysEndpoint) + .NotEmpty() + .WithMessage("Keys endpoint is required") + .Matches($"^{discUrl.Scheme}://{discUrl.Host}") + .WithMessage("Keys endpoint must be on the same domain as the discovery url"); + } + } +} diff --git a/plugins/VNLib.Plugins.Essentials.Auth.Social/src/openid/OpenIdResolver.cs b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/openid/OpenIdResolver.cs new file mode 100644 index 0000000..652b622 --- /dev/null +++ b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/openid/OpenIdResolver.cs @@ -0,0 +1,67 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials.Auth.Social +* File: OauthClientConfig.cs +* +* OauthClientConfig.cs is part of VNLib.Plugins.Essentials.Auth.Social which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials.Auth.Social 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.Auth.Social 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.Threading.Tasks; +using System.Threading; +using VNLib.Net.Rest.Client.Construction; +using RestSharp; + +namespace VNLib.Plugins.Essentials.Auth.Social.openid +{ + /// <summary> + /// Resolves the openid connect configuration from a discovery url + /// </summary> + public sealed class OpenIdResolver + { + private readonly RestSiteAdapterBase _defaultAdapter = RestSiteAdapterBase.CreateSimpleAdapter(); + + /// <summary> + /// Initializes a new instance of the <see cref="OpenIdResolver"/> class + /// </summary> + public OpenIdResolver() + { + _defaultAdapter.DefineSingleEndpoint() + .WithEndpoint<DiscoveryRequest>() + .WithMethod(Method.Get) + .WithUrl(m => m.DiscoUrl) + .WithHeader("Accept", "application/json") + .OnResponse((r, rr) => rr.ThrowIfError()); + } + + /// <summary> + /// Resolves the openid connect configuration from the discovery url + /// </summary> + /// <param name="discoveryUrl">The openid connect discovery url</param> + /// <param name="cancellation">A token to cancel the resolution operation</param> + /// <returns>A task that resolves the openid connect configuration data</returns> + public Task<OpenIdPortalConfig?> ResolveAsync(string discoveryUrl, CancellationToken cancellation) + { + return _defaultAdapter.ExecuteAsync(entity: new DiscoveryRequest(discoveryUrl), cancellation) + .AsJson<OpenIdPortalConfig>(); + } + + sealed record class DiscoveryRequest(string DiscoUrl) + { } + } +} |