// 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);
}
}