add a bunch of stuff copied from Foxchat.NET

This commit is contained in:
sam 2024-05-28 15:29:18 +02:00
parent f4c0a40259
commit 6114f384a0
Signed by: sam
GPG key ID: B4EF20DDE721CAA1
21 changed files with 1216 additions and 35 deletions

View file

@ -0,0 +1,66 @@
using System.Security.Cryptography;
using Foxnouns.Backend.Database;
using Foxnouns.Backend.Database.Models;
using Foxnouns.Backend.Utils;
using Microsoft.EntityFrameworkCore;
using NodaTime;
namespace Foxnouns.Backend.Middleware;
public class AuthenticationMiddleware(DatabaseContext db, IClock clock) : IMiddleware
{
public async Task InvokeAsync(HttpContext ctx, RequestDelegate next)
{
var endpoint = ctx.GetEndpoint();
var metadata = endpoint?.Metadata.GetMetadata<AuthenticateAttribute>();
if (metadata == null)
{
await next(ctx);
return;
}
var header = ctx.Request.Headers.Authorization.ToString();
if (!OauthUtils.TryFromBase64String(header, out var rawToken))
{
await next(ctx);
return;
}
var hash = SHA512.HashData(rawToken);
var oauthToken = await db.Tokens
.Include(t => t.Application)
.Include(t => t.User)
.FirstOrDefaultAsync(t => t.Hash == hash && t.ExpiresAt > clock.GetCurrentInstant() && !t.ManuallyExpired);
if (oauthToken == null)
{
await next(ctx);
return;
}
ctx.SetToken(oauthToken);
await next(ctx);
}
}
public static class HttpContextExtensions
{
private const string Key = "token";
public static void SetToken(this HttpContext ctx, Token token) => ctx.Items.Add(Key, token);
public static User? GetUser(this HttpContext ctx) => ctx.GetToken()?.User;
public static User GetUserOrThrow(this HttpContext ctx) =>
ctx.GetUser() ?? throw new ApiError.AuthenticationError("No user in HttpContext");
public static Token? GetToken(this HttpContext ctx)
{
if (ctx.Items.TryGetValue(Key, out var token))
return token as Token;
return null;
}
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AuthenticateAttribute : Attribute;

View file

@ -0,0 +1,36 @@
using Foxnouns.Backend.Utils;
namespace Foxnouns.Backend.Middleware;
public class AuthorizationMiddleware : IMiddleware
{
public async Task InvokeAsync(HttpContext ctx, RequestDelegate next)
{
var endpoint = ctx.GetEndpoint();
var attribute = endpoint?.Metadata.GetMetadata<AuthorizeAttribute>();
if (attribute == null)
{
await next(ctx);
return;
}
var token = ctx.GetToken();
if (token == null)
throw new ApiError.Unauthorized("This endpoint requires an authenticated user.");
if (attribute.Scopes.Length > 0 && attribute.Scopes.Except(token.Scopes.ExpandScopes()).Any())
throw new ApiError.Forbidden("This endpoint requires ungranted scopes.",
attribute.Scopes.Except(token.Scopes.ExpandScopes()));
await next(ctx);
}
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AuthorizeAttribute(params string[] scopes) : Attribute
{
public readonly bool RequireAdmin = scopes.Contains(":admin");
public readonly bool RequireModerator = scopes.Contains(":moderator");
public readonly string[] Scopes = scopes.Except([":admin", ":moderator"]).ToArray();
}

View file

@ -0,0 +1,93 @@
using System.Net;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace Foxnouns.Backend.Middleware;
public class ErrorHandlerMiddleware(ILogger baseLogger) : IMiddleware
{
public async Task InvokeAsync(HttpContext ctx, RequestDelegate next)
{
try
{
await next(ctx);
}
catch (Exception e)
{
var type = e.TargetSite?.DeclaringType ?? typeof(ErrorHandlerMiddleware);
var typeName = e.TargetSite?.DeclaringType?.FullName ?? "<unknown>";
var logger = baseLogger.ForContext(type);
if (ctx.Response.HasStarted)
{
logger.Error(e, "Error in {ClassName} ({Path}) after response started being sent", typeName,
ctx.Request.Path);
return;
}
if (e is ApiError ae)
{
ctx.Response.StatusCode = (int)ae.StatusCode;
ctx.Response.Headers.RequestId = ctx.TraceIdentifier;
ctx.Response.ContentType = "application/json; charset=utf-8";
if (ae is ApiError.Forbidden fe)
{
await ctx.Response.WriteAsync(JsonConvert.SerializeObject(new HttpApiError
{
Status = (int)fe.StatusCode,
Code = ErrorCode.Forbidden,
Message = fe.Message,
Scopes = fe.Scopes.Length > 0 ? fe.Scopes : null
}));
return;
}
await ctx.Response.WriteAsync(JsonConvert.SerializeObject(new HttpApiError
{
Status = (int)ae.StatusCode,
Code = ErrorCode.GenericApiError,
Message = ae.Message,
}));
return;
}
if (e is FoxnounsError fce)
{
logger.Error(fce.Inner ?? fce, "Exception in {ClassName} ({Path})", typeName, ctx.Request.Path);
}
else
{
logger.Error(e, "Exception in {ClassName} ({Path})", typeName, ctx.Request.Path);
}
ctx.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
ctx.Response.Headers.RequestId = ctx.TraceIdentifier;
ctx.Response.ContentType = "application/json; charset=utf-8";
await ctx.Response.WriteAsync(JsonConvert.SerializeObject(new HttpApiError
{
Status = (int)HttpStatusCode.InternalServerError,
Code = ErrorCode.InternalServerError,
Message = "Internal server error",
}));
}
}
}
public record HttpApiError
{
public required int Status { get; init; }
[JsonConverter(typeof(StringEnumConverter))]
public required ErrorCode Code { get; init; }
public required string Message { get; init; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string[]? Scopes { get; init; }
}
public enum ErrorCode
{
InternalServerError,
Forbidden,
BadRequest,
AuthenticationError,
GenericApiError,
}