using Foxchat.Chat.Database; using Foxchat.Chat.Database.Models; using Foxchat.Core; using Foxchat.Core.Extensions; using Foxchat.Core.Federation; using Microsoft.EntityFrameworkCore; namespace Foxchat.Chat.Middleware; public class ServerAuthenticationMiddleware(ILogger logger, ChatContext db, RequestSigningService requestSigningService) : IMiddleware { public async Task InvokeAsync(HttpContext ctx, RequestDelegate next) { var endpoint = ctx.GetEndpoint(); // Endpoints require server authentication by default, unless they have the [Unauthenticated] attribute. var metadata = endpoint?.Metadata.GetMetadata(); if (metadata != null) { await next(ctx); return; } if (!ctx.ExtractRequestData(out var signature, out var domain, out var signatureData)) throw new ApiError.IncomingFederationError("This endpoint requires signed requests."); var instance = await GetInstanceAsync(domain); if (!requestSigningService.VerifySignature(instance.PublicKey, signature, signatureData)) throw new ApiError.IncomingFederationError("Signature is not valid."); ctx.SetSignature(instance, signatureData); await next(ctx); } private async Task GetInstanceAsync(string domain) { return await db.IdentityInstances.FirstOrDefaultAsync(i => i.Domain == domain) ?? throw new ApiError.IncomingFederationError("Remote instance is not known."); } } /// /// Attribute to be put on controllers or methods to indicate that it does not require a signed request. /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class ServerUnauthenticatedAttribute : Attribute; public static class HttpContextExtensions { private const string Key = "instance"; public static void SetSignature(this HttpContext ctx, IdentityInstance instance, SignatureData data) { ctx.Items.Add(Key, (instance, data)); } public static (IdentityInstance?, SignatureData?) GetSignature(this HttpContext ctx) { try { var obj = ctx.GetSignatureOrThrow(); return (obj.Item1, obj.Item2); } catch { return (null, null); } } public static (IdentityInstance, SignatureData) GetSignatureOrThrow(this HttpContext ctx) { if (!ctx.Items.TryGetValue(Key, out var obj)) throw new ApiError.AuthenticationError("No instance in HttpContext"); return ((IdentityInstance, SignatureData))obj!; } public static (IdentityInstance, SignatureData, string) GetSignatureWithUser(this HttpContext ctx) { var (instance, sig) = ctx.GetSignatureOrThrow(); if (sig.UserId == null) throw new ApiError.IncomingFederationError("This endpoint requires a user ID."); return (instance, sig, sig.UserId); } }