[backend/middleware] Authenticate requests with signature headers when authorized fetch is disabled
This fixes a bug that prevented fetching follower-only posts from remote instances when the local instance has authorized fetch disabled.
This commit is contained in:
parent
0442f676e1
commit
befe550f37
1 changed files with 51 additions and 34 deletions
|
@ -7,8 +7,10 @@ using Iceshrimp.Backend.Core.Extensions;
|
|||
using Iceshrimp.Backend.Core.Federation.Cryptography;
|
||||
using Iceshrimp.Backend.Core.Helpers.LibMfm.Conversion;
|
||||
using Iceshrimp.Backend.Core.Services;
|
||||
using Iceshrimp.Utils.Common;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using static Iceshrimp.Backend.Core.Federation.ActivityPub.UserResolver;
|
||||
|
||||
namespace Iceshrimp.Backend.Core.Middleware;
|
||||
|
@ -37,25 +39,46 @@ public class AuthorizedFetchMiddleware(
|
|||
mfmConverter.SupportsHtmlFormatting.Value = true;
|
||||
mfmConverter.SupportsInlineMedia.Value = true;
|
||||
|
||||
if (!config.Value.AuthorizedFetch)
|
||||
{
|
||||
await next(ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.Response.Headers.CacheControl = "private, no-store";
|
||||
|
||||
var request = ctx.Request;
|
||||
var ct = appLifetime.ApplicationStopping;
|
||||
|
||||
// Short-circuit instance & relay actor fetches
|
||||
_instanceActorUri ??= $"/users/{(await systemUserSvc.GetInstanceActorAsync()).Id}";
|
||||
_relayActorUri ??= $"/users/{(await systemUserSvc.GetRelayActorAsync()).Id}";
|
||||
if (request.Path.Value == _instanceActorUri || request.Path.Value == _relayActorUri)
|
||||
if (ctx.Request.Path.Value == _instanceActorUri || ctx.Request.Path.Value == _relayActorUri)
|
||||
{
|
||||
await next(ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent authenticated responses from ending up in caches, and prevent unauthenticated responses
|
||||
// from being returned for authenticated requests
|
||||
if (ctx.Request.Headers.ContainsKey("Signature"))
|
||||
ctx.Response.Headers.CacheControl = "private, no-store";
|
||||
else
|
||||
ctx.Response.Headers.Append(HeaderNames.Vary, "Signature");
|
||||
|
||||
var res = await ValidateSignatureAsync(ctx);
|
||||
|
||||
// We want to reject blocked instances even if authorized fetch is disabled
|
||||
if (res.TryGetError(out var error) && error is InstanceBlockedException)
|
||||
throw error;
|
||||
|
||||
if (res.TryGetResult(out var user) && user.Value != null)
|
||||
{
|
||||
ctx.SetActor(user.Value);
|
||||
}
|
||||
else if (config.Value.AuthorizedFetch)
|
||||
{
|
||||
if (error != null) throw error;
|
||||
throw new GracefulException(HttpStatusCode.Forbidden, "Request signature validation failed");
|
||||
}
|
||||
|
||||
await next(ctx);
|
||||
}
|
||||
|
||||
private async Task<Result<Optional<User>>> ValidateSignatureAsync(HttpContext ctx)
|
||||
{
|
||||
var request = ctx.Request;
|
||||
var ct = appLifetime.ApplicationStopping;
|
||||
|
||||
UserPublickey? key = null;
|
||||
var verified = false;
|
||||
|
||||
|
@ -64,17 +87,15 @@ public class AuthorizedFetchMiddleware(
|
|||
try
|
||||
{
|
||||
if (!request.Headers.TryGetValue("signature", out var sigHeader))
|
||||
throw new GracefulException(HttpStatusCode.Unauthorized, "Request is missing the signature header");
|
||||
return new GracefulException(HttpStatusCode.Unauthorized, "Request is missing the signature header");
|
||||
|
||||
var sig = HttpSignature.Parse(sigHeader.ToString());
|
||||
|
||||
if (await fedCtrlSvc.ShouldBlockAsync(sig.KeyId))
|
||||
throw new GracefulException(HttpStatusCode.Forbidden, "Forbidden", "Instance is blocked",
|
||||
suppressLog: true);
|
||||
return new InstanceBlockedException();
|
||||
|
||||
// First, we check if we already have the key
|
||||
key = await db.UserPublickeys.Include(p => p.User)
|
||||
.FirstOrDefaultAsync(p => p.KeyId == sig.KeyId, ct);
|
||||
key = await db.UserPublickeys.Include(p => p.User).FirstOrDefaultAsync(p => p.KeyId == sig.KeyId, ct);
|
||||
|
||||
// If we don't, we need to try to fetch it
|
||||
if (key == null)
|
||||
|
@ -82,31 +103,26 @@ public class AuthorizedFetchMiddleware(
|
|||
try
|
||||
{
|
||||
var user = await userResolver.ResolveAsync(sig.KeyId, ResolveFlags.Uri).WaitAsync(ct);
|
||||
key = await db.UserPublickeys.Include(p => p.User)
|
||||
.FirstOrDefaultAsync(p => p.User == user, ct);
|
||||
key = await db.UserPublickeys.Include(p => p.User).FirstOrDefaultAsync(p => p.User == user, ct);
|
||||
|
||||
// If the key is still null here, we have a data consistency issue and need to update the key manually
|
||||
key ??= await userSvc.UpdateUserPublicKeyAsync(user).WaitAsync(ct);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (e is GracefulException) throw;
|
||||
throw new Exception($"Failed to fetch key of signature user ({sig.KeyId}) - {e.Message}");
|
||||
if (e is GracefulException) return e;
|
||||
return new Exception($"Failed to fetch key of signature user ({sig.KeyId}) - {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// If we still don't have the key, something went wrong and we need to throw an exception
|
||||
if (key == null) throw new Exception($"Failed to fetch key of signature user ({sig.KeyId})");
|
||||
|
||||
if (key.User.IsSuspended)
|
||||
throw GracefulException.Forbidden("User is suspended");
|
||||
return GracefulException.Forbidden("User is suspended");
|
||||
if (key.User.IsLocalUser)
|
||||
throw new Exception("Remote user must have a host");
|
||||
return new Exception("Remote user must have a host");
|
||||
|
||||
// We want to check both the user host & the keyId host (as account & web domain might be different)
|
||||
if (await fedCtrlSvc.ShouldBlockAsync(key.User.Host, key.KeyId))
|
||||
throw new GracefulException(HttpStatusCode.Forbidden, "Forbidden", "Instance is blocked",
|
||||
suppressLog: true);
|
||||
return new InstanceBlockedException();
|
||||
|
||||
List<string> headers = ["(request-target)", "host"];
|
||||
|
||||
|
@ -128,18 +144,19 @@ public class AuthorizedFetchMiddleware(
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (e is AuthFetchException afe) throw GracefulException.Accepted(afe.Message);
|
||||
if (e is GracefulException { SuppressLog: true }) throw;
|
||||
if (e is AuthFetchException afe) return GracefulException.Accepted(afe.Message);
|
||||
if (e is GracefulException { SuppressLog: true }) return e;
|
||||
logger.LogDebug("Error validating HTTP signature: {error}", e.Message);
|
||||
}
|
||||
|
||||
if (!verified || key == null)
|
||||
throw new GracefulException(HttpStatusCode.Forbidden, "Request signature validation failed");
|
||||
return new Optional<User>(null);
|
||||
|
||||
ctx.SetActor(key.User);
|
||||
|
||||
await next(ctx);
|
||||
return new Optional<User>(key.User);
|
||||
}
|
||||
|
||||
private class InstanceBlockedException() : GracefulException(HttpStatusCode.Forbidden, "Forbidden",
|
||||
"Instance is blocked", suppressLog: true);
|
||||
}
|
||||
|
||||
public class AuthorizedFetchAttribute : Attribute;
|
||||
|
|
Loading…
Add table
Reference in a new issue