add a bunch of stuff copied from Foxchat.NET
This commit is contained in:
parent
f4c0a40259
commit
6114f384a0
21 changed files with 1216 additions and 35 deletions
66
Foxnouns.Backend/Middleware/AuthenticationMiddleware.cs
Normal file
66
Foxnouns.Backend/Middleware/AuthenticationMiddleware.cs
Normal 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;
|
36
Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs
Normal file
36
Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs
Normal 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();
|
||||
}
|
93
Foxnouns.Backend/Middleware/ErrorHandlerMiddleware.cs
Normal file
93
Foxnouns.Backend/Middleware/ErrorHandlerMiddleware.cs
Normal 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,
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue