From 3bd7effc15d0b87adce01281b073aa1db67d3cba Mon Sep 17 00:00:00 2001 From: vnugent Date: Sat, 6 Jan 2024 18:06:01 -0500 Subject: social portal conversion, pull provider libraries & include some prebuilts --- .../VNLib.Plugins.Essentials.Auth.Auth0/README.md | 15 ++ .../build.readme.md | 11 ++ .../src/Auth0Portal.cs | 67 ++++++++ .../src/Endpoints/LoginEndpoint.cs | 191 +++++++++++++++++++++ .../src/Endpoints/LogoutEndpoint.cs | 70 ++++++++ .../src/VNLib.Plugins.Essentials.Auth.Auth0.csproj | 48 ++++++ 6 files changed, 402 insertions(+) create mode 100644 plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/README.md create mode 100644 plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/build.readme.md create mode 100644 plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/src/Auth0Portal.cs create mode 100644 plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/src/Endpoints/LoginEndpoint.cs create mode 100644 plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/src/Endpoints/LogoutEndpoint.cs create mode 100644 plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/src/VNLib.Plugins.Essentials.Auth.Auth0.csproj (limited to 'plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0') diff --git a/plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/README.md b/plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/README.md new file mode 100644 index 0000000..1cacc6b --- /dev/null +++ b/plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/README.md @@ -0,0 +1,15 @@ +# VNLib.Plugins.Essentials.Auth.Auth0 +*A runtime asset library that provides enterprise Auth0 OAuth authentication to your application that is using the [Auth.Social](../../VNLib.Plugins.Essentials.Auth.Social) plugin* + +## Builds +Debug build w/ symbols & xml docs, release builds, NuGet packages, and individually packaged source code are available on my website (link below). + +## Docs and Guides +Documentation, specifications, and setup guides are available on my website. + +[Docs and Articles](https://www.vaughnnugent.com/resources/software/articles?tags=docs,_VNLib.Plugins.Essentials.Auth.Auth0) +[Builds and Source](https://www.vaughnnugent.com/resources/software/modules/Plugins.Essentials) +[Nuget Feeds](https://www.vaughnnugent.com/resources/software/modules) + +## License +Source files in for this project are licensed to you under the GNU Affero General Public License (or any later version). See the LICENSE files for more information. \ No newline at end of file diff --git a/plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/build.readme.md b/plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/build.readme.md new file mode 100644 index 0000000..4533b0d --- /dev/null +++ b/plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/build.readme.md @@ -0,0 +1,11 @@ +VNLib.Plugins.Essentials.Auth.Auth0 Copyright © 2024 Vaughn Nugent +Contact: Vaughn Nugent vnpublic[at]proton.me + +LICENSE: +You should have recieved a copy of the GNU Affero General Public License along with this program. If not, see . + +In the future there will be more information for installtion in this file, but for now go to my website + +https://www.vaughnnugent.com/resources/software/articles?tags=docs,_VNLib.Plugins.Essentials.Auth.Auth0 + +Thank you! \ No newline at end of file diff --git a/plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/src/Auth0Portal.cs b/plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/src/Auth0Portal.cs new file mode 100644 index 0000000..0ae92f4 --- /dev/null +++ b/plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/src/Auth0Portal.cs @@ -0,0 +1,67 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials.Auth.Auth0 +* File: Auth0Portal.cs +* +* Auth0Portal.cs is part of VNLib.Plugins.Essentials.Auth.Auth0 which is +* part of the larger VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials.Auth.Auth0 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.Auth0 is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; + +using VNLib.Plugins.Extensions.Loading; +using VNLib.Plugins.Extensions.Loading.Routing; +using VNLib.Plugins.Essentials.Auth.Social; + +using VNLib.Plugins.Essentials.Auth.Auth0.Endpoints; + +namespace VNLib.Plugins.Essentials.Auth.Auth0 +{ + + [ServiceExport] + [ConfigurationName(ConfigKey)] + public sealed class Auth0Portal : IOAuthProvider + { + internal const string ConfigKey = "auth0"; + + private readonly LoginEndpoint _loginEndpoint; + private readonly LogoutEndpoint _logoutEndpoint; + + public Auth0Portal(PluginBase plugin, IConfigScope config) + { + //Init the login endpoint + _loginEndpoint = plugin.Route(); + _logoutEndpoint = plugin.Route(); + } + + /// + public SocialOAuthPortal[] GetPortals() + { + + //Return the Auth0 portal + return [ + new SocialOAuthPortal( + ConfigKey, + _loginEndpoint, + _logoutEndpoint + ) + ]; + + } + } +} diff --git a/plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/src/Endpoints/LoginEndpoint.cs b/plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/src/Endpoints/LoginEndpoint.cs new file mode 100644 index 0000000..52be461 --- /dev/null +++ b/plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/src/Endpoints/LoginEndpoint.cs @@ -0,0 +1,191 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials.Auth.Auth0 +* File: LoginEndpoint.cs +* +* LoginEndpoint.cs is part of VNLib.Plugins.Essentials.Auth.Auth0 which is +* part of the larger VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials.Auth.Auth0 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.Auth0 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.Json; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; + +using RestSharp; + +using VNLib.Hashing; +using VNLib.Hashing.IdentityUtility; +using VNLib.Utils.Logging; +using VNLib.Plugins.Essentials.Accounts; +using VNLib.Plugins.Extensions.Loading; +using VNLib.Net.Rest.Client.Construction; +using VNLib.Plugins.Essentials.Auth.Social; + +/* + * Provides specialized login for Auth0 identity managment system. Auth0 apis use JWT tokens + * and JWK signing keys. Keys are downloaded when the plugin is first loaded and cached for + * the lifetime of the plugin. The keys are used to verify the JWT token and extract the user + */ + +namespace VNLib.Plugins.Essentials.Auth.Auth0.Endpoints +{ + + [ConfigurationName(Auth0Portal.ConfigKey)] + internal sealed class LoginEndpoint : SocialOauthBase + { + private readonly IAsyncLazy Auth0VerificationJwk; + private readonly bool VerifyEmail; + + public LoginEndpoint(PluginBase plugin, IConfigScope config) : base(plugin, config) + { + string keyUrl = config["key_url"].GetString() ?? throw new KeyNotFoundException("Missing Auth0 'key_url' from config"); + + //Define the key endpoint + SiteAdapter.DefineSingleEndpoint() + .WithEndpoint() + .WithUrl(keyUrl) + .WithMethod(Method.Get) + .WithHeader("Accept", "application/json") + .OnResponse((r, res) => res.ThrowIfError()); + + //Check for email verification + VerifyEmail = config.TryGetValue("verified_email", out JsonElement el) && el.GetBoolean(); + + //Get certificate on background thread + Auth0VerificationJwk = Task.Run(GetRsaCertificate).AsLazy(); + } + + private async Task GetRsaCertificate() + { + try + { + Log.Debug("Getting Auth0 signing keys"); + + //rent client from pool + RestResponse response = await SiteAdapter.ExecuteAsync(new GetKeyRequest()); + + //Get response as doc + using JsonDocument doc = JsonDocument.Parse(response.RawBytes); + + //Create a new jwk from each key element in the response + ReadOnlyJsonWebKey[] keys = doc.RootElement.GetProperty("keys") + .EnumerateArray() + .Select(static k => new ReadOnlyJsonWebKey(k)) + .ToArray(); + + Log.Debug("Found {count} Auth0 signing keys", keys.Length); + + return keys; + } + catch (Exception e) + { + Log.Error(e, "Failed to get Auth0 signing keys"); + throw; + } + } + + /* + * Auth0 uses the format "platoform|{user_id}" for the user id so it should match the + * external platofrm as github and discord endoints also + */ + + private static string GetUserIdFromPlatform(string userName) + { + return ManagedHash.ComputeHash(userName, HashAlg.SHA1, HashEncodingMode.Hexadecimal); + } + + + private static readonly Task EmptyLoginData = Task.FromResult(null); + private static readonly Task EmptyUserData = Task.FromResult(null); + + /// + protected override Task GetLoginDataAsync(IOAuthAccessState clientAccess, CancellationToken cancellation) + { + //recover the identity token + using JsonWebToken jwt = JsonWebToken.Parse(clientAccess.IdToken); + + //Verify the token against the first signing key + if (!jwt.VerifyFromJwk(Auth0VerificationJwk.Value[0])) + { + 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.Value.Equals(audience, StringComparison.Ordinal)) + { + //Invalid audience + return EmptyLoginData; + } + + return Task.FromResult(new UserLoginData() + { + UserId = GetUserIdFromPlatform(userId) + }); + } + + /* + * 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 GetAccountDataAsync(IOAuthAccessState clientAccess, CancellationToken cancellationToken) + { + //Parse token again to get the user data + using JsonWebToken jwt = JsonWebToken.Parse(clientAccess.IdToken); + + using JsonDocument userData = jwt.GetPayload(); + + //Confirm email is verified + if (!userData.RootElement.GetProperty("email_verified").GetBoolean() && VerifyEmail) + { + return EmptyUserData; + } + + string fullName = userData.RootElement.GetProperty("name").GetString() ?? " "; + + return Task.FromResult(new AccountData() + { + EmailAddress = userData.RootElement.GetProperty("email").GetString(), + First = fullName.Split(' ').FirstOrDefault(), + Last = fullName.Split(' ').LastOrDefault(), + }); + } + + private sealed record class GetKeyRequest() + { } + } +} diff --git a/plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/src/Endpoints/LogoutEndpoint.cs b/plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/src/Endpoints/LogoutEndpoint.cs new file mode 100644 index 0000000..497357a --- /dev/null +++ b/plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/src/Endpoints/LogoutEndpoint.cs @@ -0,0 +1,70 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials.Auth.Auth0 +* File: LogoutEndpoint.cs +* +* LogoutEndpoint.cs is part of VNLib.Plugins.Essentials.Auth.Auth0 which is +* part of the larger VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials.Auth.Auth0 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.Auth0 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 VNLib.Utils; + +using VNLib.Plugins.Extensions.Loading; +using VNLib.Plugins.Essentials.Accounts; +using VNLib.Plugins.Essentials.Endpoints; +using VNLib.Plugins.Essentials.Extensions; + + +namespace VNLib.Plugins.Essentials.Auth.Auth0.Endpoints +{ + [ConfigurationName(Auth0Portal.ConfigKey)] + internal sealed class LogoutEndpoint : ProtectedWebEndpoint + { + private readonly IAsyncLazy ReturnUrl; + + public LogoutEndpoint(PluginBase plugin, IConfigScope config) + { + string returnToUrl = config.GetRequiredProperty("return_to_url", p => p.GetString()!); + string logoutUrl = config.GetRequiredProperty("logout_url", p => p.GetString()!); + string path = config.GetRequiredProperty("path", p => p.GetString()!); + + InitPathAndLog($"{path}/logout", plugin.Log); + + //Build the return url once the client id is available + ReturnUrl = plugin.GetSecretAsync("auth0_client_id").ToLazy(sr => + { + return $"{logoutUrl}?client_id={sr.Result.ToString()}&returnTo={returnToUrl}"; + }); + } + + protected override ERRNO PreProccess(HttpEntity entity) + { + //Client required to be fully authorized + return base.PreProccess(entity) + && entity.IsClientAuthorized(AuthorzationCheckLevel.Critical); + } + + protected override VfReturnType Post(HttpEntity entity) + { + //Invalidate the login before redirecting the client + entity.InvalidateLogin(); + entity.Redirect(RedirectType.Temporary, ReturnUrl.Value); + return VfReturnType.VirtualSkip; + } + } +} \ No newline at end of file diff --git a/plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/src/VNLib.Plugins.Essentials.Auth.Auth0.csproj b/plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/src/VNLib.Plugins.Essentials.Auth.Auth0.csproj new file mode 100644 index 0000000..2beb64f --- /dev/null +++ b/plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0/src/VNLib.Plugins.Essentials.Auth.Auth0.csproj @@ -0,0 +1,48 @@ + + + + enable + net8.0 + VNLib.Plugins.Essentials.Auth.Auth0 + VNLib.Plugins.Essentials.Auth.Auth0 + True + latest-all + en-US + true + + + + Vaughn Nugent + Vaughn Nugent + VNLib.Plugins.Essentials.Auth.Auth0 + VNLib.Plugins.Essentials.Auth.Auth0 + A runtime asset library that adds Auth0 social OAuth autentication integration with Auth.Social plugin library + Copyright © 2024 Vaughn Nugent + https://www.vaughnnugent.com/resources/software/modules/Plugins.Essentials + https://Auth0.com/VnUgE/Plugins.Essentials/tree/master/plugins/providers/VNLib.Plugins.Essentials.Auth.Auth0 + README.md + LICENSE + True + + + + + True + \ + Always + + + True + \ + + + + + + + + + + + + -- cgit