using Foxchat.Chat.Database; using Foxchat.Chat.Database.Models; using Foxchat.Core; using Foxchat.Core.Federation; using Microsoft.EntityFrameworkCore; namespace Foxchat.Chat.Middleware; public class AuthenticationMiddleware(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 (!ExtractRequestData(ctx, 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."); } private bool ExtractRequestData(HttpContext ctx, out string signature, out string domain, out SignatureData data) { signature = string.Empty; domain = string.Empty; data = SignatureData.Empty; if (!ctx.Request.Headers.TryGetValue(RequestSigningService.SIGNATURE_HEADER, out var encodedSignature)) return false; if (!ctx.Request.Headers.TryGetValue(RequestSigningService.DATE_HEADER, out var date)) return false; if (!ctx.Request.Headers.TryGetValue(RequestSigningService.SERVER_HEADER, out var server)) return false; var time = RequestSigningService.ParseTime(date.ToString()); string? userId = null; if (ctx.Request.Headers.TryGetValue(RequestSigningService.USER_HEADER, out var userIdHeader)) userId = userIdHeader; var host = ctx.Request.Headers.Host.ToString(); signature = encodedSignature.ToString(); domain = server.ToString(); data = new SignatureData( time, host, ctx.Request.Path, (int?)ctx.Request.Headers.ContentLength, userId ); return true; } } [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class UnauthenticatedAttribute : 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!; } }