add error handler middleware
This commit is contained in:
parent
41e4dda7b4
commit
7a0247b551
13 changed files with 177 additions and 46 deletions
77
Foxchat.Identity/Middleware/AuthenticationMiddleware.cs
Normal file
77
Foxchat.Identity/Middleware/AuthenticationMiddleware.cs
Normal file
|
@ -0,0 +1,77 @@
|
|||
using System.Security.Cryptography;
|
||||
using Foxchat.Core;
|
||||
using Foxchat.Core.Utils;
|
||||
using Foxchat.Identity.Database;
|
||||
using Foxchat.Identity.Database.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace Foxchat.Identity.Middleware;
|
||||
|
||||
public class AuthenticationMiddleware(
|
||||
IdentityContext 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 (!header.StartsWith("bearer ", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
await next(ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
var token = header[7..];
|
||||
|
||||
if (!CryptoUtils.TryFromBase64String(token, out var rawToken))
|
||||
{
|
||||
await next(ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
var hash = SHA512.HashData(rawToken);
|
||||
var oauthToken = await db.Tokens
|
||||
.Include(t => t.Account)
|
||||
.Include(t => t.Application)
|
||||
.FirstOrDefaultAsync(t => t.Hash == hash && t.Expires > clock.GetCurrentInstant());
|
||||
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 Account? GetAccount(this HttpContext ctx) => ctx.GetToken()?.Account;
|
||||
public static Account GetAccountOrThrow(this HttpContext ctx) =>
|
||||
ctx.GetAccount() ?? throw new ApiError.AuthenticationError("No account 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;
|
37
Foxchat.Identity/Middleware/AuthorizationMiddleware.cs
Normal file
37
Foxchat.Identity/Middleware/AuthorizationMiddleware.cs
Normal file
|
@ -0,0 +1,37 @@
|
|||
using Foxchat.Core;
|
||||
using Foxchat.Identity.Database;
|
||||
using NodaTime;
|
||||
|
||||
namespace Foxchat.Identity.Middleware;
|
||||
|
||||
public class AuthorizationMiddleware(
|
||||
IdentityContext db,
|
||||
IClock clock
|
||||
) : 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 || token.Expires > clock.GetCurrentInstant())
|
||||
throw new ApiError.Unauthorized("This endpoint requires an authenticated user.");
|
||||
if (attribute.Scopes.Length > 0 && attribute.Scopes.Except(token.Scopes).Any())
|
||||
throw new ApiError.Forbidden("This endpoint requires ungranted scopes.", attribute.Scopes.Except(token.Scopes));
|
||||
|
||||
await next(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
|
||||
public class AuthorizeAttribute(params string[] scopes) : Attribute
|
||||
{
|
||||
public readonly string[] Scopes = scopes;
|
||||
}
|
87
Foxchat.Identity/Middleware/ErrorHandlerMiddleware.cs
Normal file
87
Foxchat.Identity/Middleware/ErrorHandlerMiddleware.cs
Normal file
|
@ -0,0 +1,87 @@
|
|||
|
||||
using System.Net;
|
||||
using Foxchat.Core;
|
||||
using Foxchat.Core.Models.Http;
|
||||
using Newtonsoft.Json;
|
||||
using ApiError = Foxchat.Core.ApiError;
|
||||
using HttpApiError = Foxchat.Core.Models.Http.ApiError;
|
||||
|
||||
namespace Foxchat.Identity.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);
|
||||
}
|
||||
|
||||
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.OutgoingFederationError ofe)
|
||||
{
|
||||
await ctx.Response.WriteAsync(JsonConvert.SerializeObject(new HttpApiError
|
||||
{
|
||||
Status = (int)ofe.StatusCode,
|
||||
Code = ErrorCode.OutgoingFederationError,
|
||||
Message = ofe.Message,
|
||||
OriginalError = ofe.InnerError
|
||||
}));
|
||||
return;
|
||||
}
|
||||
else 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 FoxchatError 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",
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue