2024-05-21 16:41:01 +02:00
|
|
|
using Foxchat.Chat.Database;
|
|
|
|
using Foxchat.Chat.Database.Models;
|
|
|
|
using Foxchat.Core;
|
2024-05-21 17:45:35 +02:00
|
|
|
using Foxchat.Core.Extensions;
|
2024-05-21 16:41:01 +02:00
|
|
|
using Foxchat.Core.Federation;
|
|
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
|
|
|
|
namespace Foxchat.Chat.Middleware;
|
|
|
|
|
2024-05-21 20:14:52 +02:00
|
|
|
public class ServerAuthenticationMiddleware(ILogger logger, ChatContext db, RequestSigningService requestSigningService)
|
2024-05-21 16:41:01 +02:00
|
|
|
: 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.
|
2024-05-21 20:14:52 +02:00
|
|
|
var metadata = endpoint?.Metadata.GetMetadata<ServerUnauthenticatedAttribute>();
|
2024-05-21 16:41:01 +02:00
|
|
|
if (metadata != null)
|
|
|
|
{
|
|
|
|
await next(ctx);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-05-21 17:45:35 +02:00
|
|
|
if (!ctx.ExtractRequestData(out var signature, out var domain, out var signatureData))
|
2024-05-21 16:41:01 +02:00
|
|
|
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<IdentityInstance> GetInstanceAsync(string domain)
|
|
|
|
{
|
|
|
|
return await db.IdentityInstances.FirstOrDefaultAsync(i => i.Domain == domain)
|
|
|
|
?? throw new ApiError.IncomingFederationError("Remote instance is not known.");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-05-21 20:14:52 +02:00
|
|
|
/// <summary>
|
|
|
|
/// Attribute to be put on controllers or methods to indicate that it does <i>not</i> require a signed request.
|
|
|
|
/// </summary>
|
2024-05-21 16:41:01 +02:00
|
|
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
|
2024-05-21 20:14:52 +02:00
|
|
|
public class ServerUnauthenticatedAttribute : Attribute;
|
2024-05-21 16:41:01 +02:00
|
|
|
|
|
|
|
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!;
|
|
|
|
}
|
|
|
|
}
|