add error handler middleware

This commit is contained in:
sam 2024-05-20 19:42:04 +02:00
parent 41e4dda7b4
commit 7a0247b551
13 changed files with 177 additions and 46 deletions

View file

@ -1,8 +1,9 @@
using Foxchat.Core;
using Foxchat.Core.Models.Http;
using Foxchat.Identity.Authorization;
using Foxchat.Identity.Middleware;
using Foxchat.Identity.Database;
using Foxchat.Identity.Database.Models;
using Foxchat.Identity.Utils;
using Microsoft.AspNetCore.Mvc;
namespace Foxchat.Identity.Controllers.Oauth;
@ -29,9 +30,7 @@ public class AppsController(ILogger logger, IdentityContext db) : ControllerBase
[HttpGet]
public IActionResult GetSelfApp([FromQuery(Name = "with_secret")] bool withSecret)
{
var token = HttpContext.GetToken();
if (token is not { Account: null }) throw new ApiError.Forbidden("This endpoint requires a client token.");
var app = token.Application;
var app = HttpContext.GetApplicationOrThrow();
return Ok(new Apps.GetSelfResponse(
app.Id,

View file

@ -0,0 +1,22 @@
using Foxchat.Identity.Middleware;
using Foxchat.Identity.Database;
using Foxchat.Identity.Utils;
using Microsoft.AspNetCore.Mvc;
namespace Foxchat.Identity.Controllers.Oauth;
[ApiController]
[Authenticate]
[Route("/_fox/ident/oauth/password")]
public class PasswordAuthController(ILogger logger, IdentityContext db) : ControllerBase
{
[HttpPost("register")]
public async Task<IActionResult> Register()
{
var app = HttpContext.GetApplicationOrThrow();
throw new NotImplementedException();
}
public record RegisterRequest();
}

View file

@ -16,7 +16,7 @@ public class TokenController(ILogger logger, IdentityContext db, IClock clock) :
var app = await db.GetApplicationAsync(req.ClientId, req.ClientSecret);
var scopes = req.Scope.Split(' ');
if (app.Scopes.Except(scopes).Any())
if (scopes.Except(app.Scopes).Any())
{
throw new ApiError.BadRequest("Invalid or unauthorized scopes");
}

View file

@ -1,4 +1,4 @@
using Foxchat.Identity.Authorization;
using Foxchat.Identity.Middleware;
namespace Foxchat.Identity.Extensions;
@ -7,6 +7,7 @@ public static class WebApplicationExtensions
public static IServiceCollection AddCustomMiddleware(this IServiceCollection services)
{
return services
.AddScoped<ErrorHandlerMiddleware>()
.AddScoped<AuthenticationMiddleware>()
.AddScoped<AuthorizationMiddleware>();
}
@ -14,6 +15,7 @@ public static class WebApplicationExtensions
public static IApplicationBuilder UseCustomMiddleware(this IApplicationBuilder app)
{
return app
.UseMiddleware<ErrorHandlerMiddleware>()
.UseMiddleware<AuthenticationMiddleware>()
.UseMiddleware<AuthorizationMiddleware>();
}

View file

@ -6,7 +6,7 @@ using Foxchat.Identity.Database.Models;
using Microsoft.EntityFrameworkCore;
using NodaTime;
namespace Foxchat.Identity.Authorization;
namespace Foxchat.Identity.Middleware;
public class AuthenticationMiddleware(
IdentityContext db,

View file

@ -1,9 +1,8 @@
using System.Net;
using Foxchat.Core;
using Foxchat.Identity.Database;
using NodaTime;
namespace Foxchat.Identity.Authorization;
namespace Foxchat.Identity.Middleware;
public class AuthorizationMiddleware(
IdentityContext db,

View 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",
}));
}
}
}

View file

@ -5,6 +5,7 @@ using Foxchat.Identity;
using Foxchat.Identity.Database;
using Foxchat.Identity.Services;
using Foxchat.Identity.Extensions;
using Newtonsoft.Json;
var builder = WebApplication.CreateBuilder(args);
@ -15,6 +16,15 @@ builder.AddSerilog(config.LogEventLevel);
await BuildInfo.ReadBuildInfo();
Log.Information("Starting Foxchat.Identity {Version} ({Hash})", BuildInfo.Version, BuildInfo.Hash);
// Set the default converter to snake case as we use it in a couple places.
JsonConvert.DefaultSettings = () => new JsonSerializerSettings
{
ContractResolver = new DefaultContractResolver
{
NamingStrategy = new SnakeCaseNamingStrategy()
}
};
builder.Services
.AddControllers()
.AddNewtonsoftJson(options =>

View file

@ -1,3 +1,7 @@
using Foxchat.Core;
using Foxchat.Identity.Middleware;
using Foxchat.Identity.Database.Models;
namespace Foxchat.Identity.Utils;
public static class OauthUtils
@ -20,4 +24,11 @@ public static class OauthUtils
return false;
}
}
public static Application GetApplicationOrThrow(this HttpContext context)
{
var token = context.GetToken();
if (token is not { Account: null }) throw new ApiError.Forbidden("This endpoint requires a client token.");
return token.Application;
}
}