From 2cbd35be45dfaa4040626eaddd9c52c368a59bb2 Mon Sep 17 00:00:00 2001 From: vnugent Date: Thu, 16 May 2024 22:11:33 -0400 Subject: first ideas on an iframe widget for favorites --- back-end/src/Model/Widget/WidgetAuthManager.cs | 152 +++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 back-end/src/Model/Widget/WidgetAuthManager.cs (limited to 'back-end/src/Model/Widget/WidgetAuthManager.cs') diff --git a/back-end/src/Model/Widget/WidgetAuthManager.cs b/back-end/src/Model/Widget/WidgetAuthManager.cs new file mode 100644 index 0000000..31e0037 --- /dev/null +++ b/back-end/src/Model/Widget/WidgetAuthManager.cs @@ -0,0 +1,152 @@ +// Copyright (C) 2024 Vaughn Nugent +// +// This program 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. +// +// This program 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 . + +using System; +using System.Text.Json; +using System.Threading.Tasks; +using System.Collections.Generic; + +using VNLib.Hashing; +using VNLib.Hashing.IdentityUtility; +using VNLib.Utils; +using VNLib.Utils.Extensions; +using VNLib.Plugins; +using VNLib.Plugins.Essentials; +using VNLib.Plugins.Essentials.Sessions; +using VNLib.Plugins.Essentials.Users; +using VNLib.Plugins.Extensions.Loading; +using VNLib.Plugins.Extensions.Loading.Users; +using VNLib.Plugins.Essentials.Extensions; + +namespace SimpleBookmark.Model.Widget +{ + [ConfigurationName("widgets")] + internal sealed class WidgetAuthManager(PluginBase plugin, IConfigScope config) + { + /* + * This auth manager attempts to cache auth data by storing user-related data in + * the session, instead of hitting the database on every request. This is done to + * reduce the number of database queries and improve performance. + */ + + const string AuthTokenQueryArgName = "t"; + const string TimestampKey = "sb.w.ts"; + const string SignatureKey = "sb.w.sig"; + const string UserSigningKeyKey = "sb.w.usk"; + + private static readonly TimeSpan _authCacheValidFor = TimeSpan.FromMinutes(25); + + private readonly IUserManager users = plugin.GetOrCreateSingleton(); + + private readonly int SigningKeySize = config.GetValueOrDefault("key_size", static p => p.GetInt32(), 16); + + public async ValueTask IsTokenValidAsync(HttpEntity entity) + { + string? token = GetTokenString(entity); + + if (string.IsNullOrWhiteSpace(token)) + { + //No token provided + return false; + } + + if (HasExistingAuth(entity, token)) + { + return true; + } + + return await AuthorizeSession(entity, token); + } + + private static bool HasExistingAuth(HttpEntity entity, string token) + { + //New sessions cannot be trusted yet + if (entity.Session.IsNew) + { + return false; + } + + /* + * See if existing auth data exists in the session + */ + DateTimeOffset expires = GetTimestamp(in entity.Session); + + if (expires < entity.RequestedTimeUtc) + { + return false; + } + + string cachedToken = GetCachedToken(in entity.Session); + + return string.Equals(cachedToken, token, StringComparison.Ordinal); + } + + private async Task AuthorizeSession(HttpEntity entity, string token) + { + try + { + using JsonWebToken jwt = JsonWebToken.Parse(token); + using JsonDocument jwtData = jwt.GetPayload(); + + string? userId = jwtData.RootElement.GetPropString("sub"); + + if (string.IsNullOrWhiteSpace(userId)) + { + return false; + } + + using IUser? user = await users.GetUserFromIDAsync(userId, entity.EventCancellation); + + return false; + } + catch (FormatException) + { + return false; + } + } + + public void GenerateWidgetKey(IUser user) + { + /* + * A base32 number ensures easy serialization by the user-system + * instead of base64 which can cause json overhead + */ + user[UserSigningKeyKey] = RandomHash.GetRandomBase32(SigningKeySize); + } + + /// + /// Gets a value that indicates if the user has a widget signing key enabled + /// + /// The user instance to verify + /// True if the user has a widget signing key enabled + public static bool HasSigningKey(IUser user) => !string.IsNullOrEmpty(user[UserSigningKeyKey]); + + private static DateTimeOffset GetTimestamp(ref readonly SessionInfo session) + { + long timestamp = VnEncoding.FromBase32String(session[TimestampKey]); + return DateTimeOffset.FromUnixTimeSeconds(timestamp); + } + + private static void SetTimestamp(ref readonly SessionInfo session, DateTimeOffset timestamp) + => session[TimestampKey] = VnEncoding.ToBase32String(timestamp.ToUnixTimeSeconds()); + + private static string GetCachedToken(ref readonly SessionInfo session) => session[SignatureKey]; + + private static void SetSignature(ref readonly SessionInfo session, string signature) => session[SignatureKey] = signature; + + private static string? GetTokenString(HttpEntity entity) => entity.QueryArgs.GetValueOrDefault(AuthTokenQueryArgName); + + } +} -- cgit