diff --git a/Iceshrimp.Backend/Controllers/Mastodon/Attributes/MastodonApiControllerAttribute.cs b/Iceshrimp.Backend/Controllers/Mastodon/Attributes/MastodonApiControllerAttribute.cs new file mode 100644 index 00000000..57abc14b --- /dev/null +++ b/Iceshrimp.Backend/Controllers/Mastodon/Attributes/MastodonApiControllerAttribute.cs @@ -0,0 +1,5 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Iceshrimp.Backend.Controllers.Mastodon.Attributes; + +public class MastodonApiControllerAttribute : ApiControllerAttribute; \ No newline at end of file diff --git a/Iceshrimp.Backend/Controllers/Mastodon/MastodonAccountController.cs b/Iceshrimp.Backend/Controllers/Mastodon/MastodonAccountController.cs index d53f8485..a77173b2 100644 --- a/Iceshrimp.Backend/Controllers/Mastodon/MastodonAccountController.cs +++ b/Iceshrimp.Backend/Controllers/Mastodon/MastodonAccountController.cs @@ -1,3 +1,4 @@ +using Iceshrimp.Backend.Controllers.Mastodon.Attributes; using Iceshrimp.Backend.Controllers.Mastodon.Renderers; using Iceshrimp.Backend.Controllers.Mastodon.Schemas; using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities; @@ -10,21 +11,20 @@ using Microsoft.EntityFrameworkCore; namespace Iceshrimp.Backend.Controllers.Mastodon; -[ApiController] -[Tags("Mastodon")] +[MastodonApiController] [Route("/api/v1/accounts")] -[AuthenticateOauth] +[Authenticate] [EnableRateLimiting("sliding")] [Produces("application/json")] public class MastodonAccountController(DatabaseContext db, UserRenderer userRenderer) : Controller { - [AuthorizeOauth("read:accounts")] + [Authorize("read:accounts")] [HttpGet("verify_credentials")] [Produces("application/json")] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Account))] [ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(MastodonErrorResponse))] [ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(MastodonErrorResponse))] public async Task VerifyUserCredentials() { - var user = HttpContext.GetOauthUser() ?? throw new GracefulException("Failed to get user from HttpContext"); + var user = HttpContext.GetUser() ?? throw new GracefulException("Failed to get user from HttpContext"); var res = await userRenderer.RenderAsync(user); return Ok(res); } diff --git a/Iceshrimp.Backend/Controllers/Mastodon/MastodonAuthController.cs b/Iceshrimp.Backend/Controllers/Mastodon/MastodonAuthController.cs index 5759de4f..b6538a0a 100644 --- a/Iceshrimp.Backend/Controllers/Mastodon/MastodonAuthController.cs +++ b/Iceshrimp.Backend/Controllers/Mastodon/MastodonAuthController.cs @@ -1,3 +1,4 @@ +using Iceshrimp.Backend.Controllers.Mastodon.Attributes; using Iceshrimp.Backend.Controllers.Mastodon.Schemas; using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Database.Tables; @@ -10,13 +11,12 @@ using Microsoft.EntityFrameworkCore; namespace Iceshrimp.Backend.Controllers.Mastodon; -[ApiController] -[Tags("Mastodon")] +[MastodonApiController] [EnableRateLimiting("sliding")] [Produces("application/json")] public class MastodonAuthController(DatabaseContext db) : Controller { [HttpGet("/api/v1/apps/verify_credentials")] - [AuthenticateOauth] + [Authenticate] [Produces("application/json")] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(AuthSchemas.VerifyAppCredentialsResponse))] [ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(MastodonErrorResponse))] diff --git a/Iceshrimp.Backend/Controllers/Mastodon/MastodonStatusController.cs b/Iceshrimp.Backend/Controllers/Mastodon/MastodonStatusController.cs index 578b8476..db613eb6 100644 --- a/Iceshrimp.Backend/Controllers/Mastodon/MastodonStatusController.cs +++ b/Iceshrimp.Backend/Controllers/Mastodon/MastodonStatusController.cs @@ -1,3 +1,4 @@ +using Iceshrimp.Backend.Controllers.Mastodon.Attributes; using Iceshrimp.Backend.Controllers.Mastodon.Renderers; using Iceshrimp.Backend.Controllers.Mastodon.Schemas; using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities; @@ -10,15 +11,14 @@ using Microsoft.EntityFrameworkCore; namespace Iceshrimp.Backend.Controllers.Mastodon; -[ApiController] -[Tags("Mastodon")] +[MastodonApiController] [Route("/api/v1/statuses")] -[AuthenticateOauth] +[Authenticate] [EnableRateLimiting("sliding")] [Produces("application/json")] public class MastodonStatusController(DatabaseContext db, NoteRenderer noteRenderer) : Controller { [HttpGet("{id}")] - [AuthenticateOauth("read:statuses")] + [Authenticate("read:statuses")] [Produces("application/json")] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Account))] [ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))] diff --git a/Iceshrimp.Backend/Controllers/Mastodon/MastodonTimelineController.cs b/Iceshrimp.Backend/Controllers/Mastodon/MastodonTimelineController.cs index 3c033018..35d05a4c 100644 --- a/Iceshrimp.Backend/Controllers/Mastodon/MastodonTimelineController.cs +++ b/Iceshrimp.Backend/Controllers/Mastodon/MastodonTimelineController.cs @@ -1,4 +1,5 @@ using Iceshrimp.Backend.Controllers.Attributes; +using Iceshrimp.Backend.Controllers.Mastodon.Attributes; using Iceshrimp.Backend.Controllers.Mastodon.Renderers; using Iceshrimp.Backend.Controllers.Mastodon.Schemas; using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities; @@ -11,22 +12,21 @@ using Microsoft.AspNetCore.RateLimiting; namespace Iceshrimp.Backend.Controllers.Mastodon; -[ApiController] -[Tags("Mastodon")] +[MastodonApiController] [Route("/api/v1/timelines")] -[AuthenticateOauth] +[Authenticate] [LinkPagination(20, 40)] [EnableRateLimiting("sliding")] [Produces("application/json")] [ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(MastodonErrorResponse))] [ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(MastodonErrorResponse))] public class MastodonTimelineController(DatabaseContext db, NoteRenderer noteRenderer) : Controller { - [AuthorizeOauth("read:statuses")] + [Authorize("read:statuses")] [HttpGet("home")] [Produces("application/json")] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable))] public async Task GetHomeTimeline(PaginationQuery query) { - var user = HttpContext.GetOauthUser() ?? throw new GracefulException("Failed to get user from HttpContext"); + var user = HttpContext.GetUser() ?? throw new GracefulException("Failed to get user from HttpContext"); var res = await db.Notes .IncludeCommonProperties() .FilterByFollowingAndOwn(user) @@ -41,12 +41,12 @@ public class MastodonTimelineController(DatabaseContext db, NoteRenderer noteRen return Ok(res); } - [AuthorizeOauth("read:statuses")] + [Authorize("read:statuses")] [HttpGet("public")] [Produces("application/json")] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable))] public async Task GetPublicTimeline(PaginationQuery query) { - var user = HttpContext.GetOauthUser() ?? throw new GracefulException("Failed to get user from HttpContext"); + var user = HttpContext.GetUser() ?? throw new GracefulException("Failed to get user from HttpContext"); var res = await db.Notes .IncludeCommonProperties() diff --git a/Iceshrimp.Backend/Controllers/Mastodon/Schemas/MastodonErrorResponse.cs b/Iceshrimp.Backend/Controllers/Mastodon/Schemas/MastodonErrorResponse.cs index 5f649988..b6f24422 100644 --- a/Iceshrimp.Backend/Controllers/Mastodon/Schemas/MastodonErrorResponse.cs +++ b/Iceshrimp.Backend/Controllers/Mastodon/Schemas/MastodonErrorResponse.cs @@ -1,8 +1,13 @@ +using System.Text.Json.Serialization; using J = System.Text.Json.Serialization.JsonPropertyNameAttribute; +using JI = System.Text.Json.Serialization.JsonIgnoreAttribute; namespace Iceshrimp.Backend.Controllers.Mastodon.Schemas; public class MastodonErrorResponse { - [J("error")] public required string Error { get; set; } - [J("error_description")] public required string? Description { get; set; } + [J("error")] public required string Error { get; set; } + + [J("error_description")] + [JI(Condition = JsonIgnoreCondition.WhenWritingNull)] + public required string? Description { get; set; } } \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Extensions/ServiceExtensions.cs b/Iceshrimp.Backend/Core/Extensions/ServiceExtensions.cs index 8c217297..f0b2165b 100644 --- a/Iceshrimp.Backend/Core/Extensions/ServiceExtensions.cs +++ b/Iceshrimp.Backend/Core/Extensions/ServiceExtensions.cs @@ -32,7 +32,6 @@ public static class ServiceExtensions { services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); //TODO: make this prettier services.AddScoped(); @@ -46,7 +45,6 @@ public static class ServiceExtensions { services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); services.AddSingleton(); // Hosted services = long running background tasks diff --git a/Iceshrimp.Backend/Core/Extensions/SwaggerGenOptionsExtensions.cs b/Iceshrimp.Backend/Core/Extensions/SwaggerGenOptionsExtensions.cs index e64b1bc1..c7351163 100644 --- a/Iceshrimp.Backend/Core/Extensions/SwaggerGenOptionsExtensions.cs +++ b/Iceshrimp.Backend/Core/Extensions/SwaggerGenOptionsExtensions.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using System.Reflection; +using Iceshrimp.Backend.Controllers.Mastodon.Attributes; using Iceshrimp.Backend.Core.Middleware; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.OpenApi.Models; @@ -11,6 +12,23 @@ public static class SwaggerGenOptionsExtensions { public static void AddOperationFilters(this SwaggerGenOptions options) { options.OperationFilter(); options.OperationFilter(); + options.OperationFilter(); + } + + [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local", + Justification = "SwaggerGenOptions.OperationFilter instantiates this class at runtime")] + private class MastodonApiControllerOperationFilter : IOperationFilter { + public void Apply(OpenApiOperation operation, OperationFilterContext context) { + if (context.MethodInfo.DeclaringType is null) + return; + + var isMastodonController = context.MethodInfo.DeclaringType.GetCustomAttributes(true) + .OfType().Any(); + + if (!isMastodonController) return; + + operation.Tags = [new OpenApiTag { Name = "Mastodon" }]; + } } [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local", @@ -26,17 +44,15 @@ public static class SwaggerGenOptionsExtensions { context.MethodInfo.GetCustomAttributes(true) .OfType().Any(); - var hasOauthAuthenticate = context.MethodInfo.DeclaringType.GetCustomAttributes(true) - .OfType().Any() || - context.MethodInfo.GetCustomAttributes(true) - .OfType().Any(); + var isMastodonController = context.MethodInfo.DeclaringType.GetCustomAttributes(true) + .OfType().Any(); - if (!hasAuthenticate && !hasOauthAuthenticate) return; + if (!hasAuthenticate) return; var schema = new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, - Id = hasAuthenticate ? "user" : "mastodon" + Id = isMastodonController ? "mastodon" : "user" } }; diff --git a/Iceshrimp.Backend/Core/Extensions/WebApplicationExtensions.cs b/Iceshrimp.Backend/Core/Extensions/WebApplicationExtensions.cs index 1b227291..08352a48 100644 --- a/Iceshrimp.Backend/Core/Extensions/WebApplicationExtensions.cs +++ b/Iceshrimp.Backend/Core/Extensions/WebApplicationExtensions.cs @@ -14,8 +14,6 @@ public static class WebApplicationExtensions { .UseMiddleware() .UseMiddleware() .UseMiddleware() - .UseMiddleware() - .UseMiddleware() .UseMiddleware(); } diff --git a/Iceshrimp.Backend/Core/Middleware/AuthenticationMiddleware.cs b/Iceshrimp.Backend/Core/Middleware/AuthenticationMiddleware.cs index cfc330b6..b5e6c93d 100644 --- a/Iceshrimp.Backend/Core/Middleware/AuthenticationMiddleware.cs +++ b/Iceshrimp.Backend/Core/Middleware/AuthenticationMiddleware.cs @@ -1,13 +1,14 @@ +using Iceshrimp.Backend.Controllers.Mastodon.Attributes; using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Database.Tables; -using Microsoft.AspNetCore.Http.Features; +using Iceshrimp.Backend.Core.Helpers; using Microsoft.EntityFrameworkCore; namespace Iceshrimp.Backend.Core.Middleware; public class AuthenticationMiddleware(DatabaseContext db) : IMiddleware { public async Task InvokeAsync(HttpContext ctx, RequestDelegate next) { - var endpoint = ctx.Features.Get()?.Endpoint; + var endpoint = ctx.GetEndpoint(); var attribute = endpoint?.Metadata.GetMetadata(); if (attribute != null) { @@ -19,27 +20,54 @@ public class AuthenticationMiddleware(DatabaseContext db) : IMiddleware { } var token = header[7..]; - var session = await db.Sessions - .Include(p => p.User) - .ThenInclude(p => p.UserProfile) - .FirstOrDefaultAsync(p => p.Token == token && p.Active); - if (session == null) { - await next(ctx); - return; + var isMastodon = endpoint?.Metadata.GetMetadata() != null; + if (isMastodon) { + var oauthToken = await db.OauthTokens + .Include(p => p.User) + .ThenInclude(p => p.UserProfile) + .Include(p => p.App) + .FirstOrDefaultAsync(p => p.Token == header && p.Active); + + if (oauthToken == null) { + await next(ctx); + return; + } + + if (attribute.Scopes.Length > 0 && + attribute.Scopes.Except(MastodonOauthHelpers.ExpandScopes(oauthToken.Scopes)).Any()) { + await next(ctx); + return; + } + + ctx.SetOauthToken(oauthToken); } + else { + var session = await db.Sessions + .Include(p => p.User) + .ThenInclude(p => p.UserProfile) + .FirstOrDefaultAsync(p => p.Token == token && p.Active); - ctx.SetSession(session); + if (session == null) { + await next(ctx); + return; + } + + ctx.SetSession(session); + } } await next(ctx); } } -public class AuthenticateAttribute : Attribute; +public class AuthenticateAttribute(params string[] scopes) : Attribute { + public readonly string[] Scopes = scopes; +} -public static partial class HttpContextExtensions { - private const string Key = "session"; +public static class HttpContextExtensions { + private const string Key = "session"; + private const string MastodonKey = "masto-session"; internal static void SetSession(this HttpContext ctx, Session session) { ctx.Items.Add(Key, session); @@ -50,8 +78,21 @@ public static partial class HttpContextExtensions { return session as Session; } + internal static void SetOauthToken(this HttpContext ctx, OauthToken session) { + ctx.Items.Add(MastodonKey, session); + } + + public static OauthToken? GetOauthToken(this HttpContext ctx) { + ctx.Items.TryGetValue(MastodonKey, out var session); + return session as OauthToken; + } + + //TODO: Is it faster to check for the MastodonApiControllerAttribute here? public static User? GetUser(this HttpContext ctx) { - ctx.Items.TryGetValue(Key, out var session); - return (session as Session)?.User; + if (ctx.Items.TryGetValue(Key, out var session)) + return (session as Session)?.User; + return ctx.Items.TryGetValue(MastodonKey, out var token) + ? (token as OauthToken)?.User + : null; } } \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Middleware/AuthorizationMiddleware.cs b/Iceshrimp.Backend/Core/Middleware/AuthorizationMiddleware.cs index 71e50bb1..843bad6f 100644 --- a/Iceshrimp.Backend/Core/Middleware/AuthorizationMiddleware.cs +++ b/Iceshrimp.Backend/Core/Middleware/AuthorizationMiddleware.cs @@ -1,19 +1,33 @@ -using Microsoft.AspNetCore.Http.Features; +using Iceshrimp.Backend.Controllers.Mastodon.Attributes; +using Iceshrimp.Backend.Core.Helpers; namespace Iceshrimp.Backend.Core.Middleware; public class AuthorizationMiddleware : IMiddleware { public async Task InvokeAsync(HttpContext ctx, RequestDelegate next) { - var endpoint = ctx.Features.Get()?.Endpoint; + var endpoint = ctx.GetEndpoint(); var attribute = endpoint?.Metadata.GetMetadata(); - if (attribute != null) - if (ctx.GetSession() is not { Active: true }) + if (attribute != null) { + var isMastodon = endpoint?.Metadata.GetMetadata() != null; + + if (isMastodon) { + var token = ctx.GetOauthToken(); + if (token is not { Active: true }) + throw GracefulException.Unauthorized("This method requires an authenticated user"); + if (attribute.Scopes.Length > 0 && + attribute.Scopes.Except(MastodonOauthHelpers.ExpandScopes(token.Scopes)).Any()) + throw GracefulException.Forbidden("This action is outside the authorized scopes"); + } + else if (ctx.GetSession() is not { Active: true }) { throw GracefulException.Forbidden("This method requires an authenticated user"); + } + } await next(ctx); } } -public class AuthorizeAttribute : Attribute; -//TODO: oauth scopes? \ No newline at end of file +public class AuthorizeAttribute(params string[] scopes) : Attribute { + public readonly string[] Scopes = scopes; +} \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Middleware/AuthorizedFetchMiddleware.cs b/Iceshrimp.Backend/Core/Middleware/AuthorizedFetchMiddleware.cs index b3561cf0..e2ed09e7 100644 --- a/Iceshrimp.Backend/Core/Middleware/AuthorizedFetchMiddleware.cs +++ b/Iceshrimp.Backend/Core/Middleware/AuthorizedFetchMiddleware.cs @@ -5,7 +5,6 @@ using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Federation.ActivityPub; using Iceshrimp.Backend.Core.Federation.Cryptography; using Iceshrimp.Backend.Core.Services; -using Microsoft.AspNetCore.Http.Features; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; @@ -19,8 +18,7 @@ public class AuthorizedFetchMiddleware( UserService userSvc, ILogger logger) : IMiddleware { public async Task InvokeAsync(HttpContext ctx, RequestDelegate next) { - var endpoint = ctx.Features.Get()?.Endpoint; - var attribute = endpoint?.Metadata.GetMetadata(); + var attribute = ctx.GetEndpoint()?.Metadata.GetMetadata(); if (attribute != null && config.Value.AuthorizedFetch) { var request = ctx.Request; diff --git a/Iceshrimp.Backend/Core/Middleware/ErrorHandlerMiddleware.cs b/Iceshrimp.Backend/Core/Middleware/ErrorHandlerMiddleware.cs index 9c948150..fcee21d4 100644 --- a/Iceshrimp.Backend/Core/Middleware/ErrorHandlerMiddleware.cs +++ b/Iceshrimp.Backend/Core/Middleware/ErrorHandlerMiddleware.cs @@ -1,5 +1,7 @@ using System.Diagnostics.CodeAnalysis; using System.Net; +using Iceshrimp.Backend.Controllers.Mastodon.Attributes; +using Iceshrimp.Backend.Controllers.Mastodon.Schemas; using Iceshrimp.Backend.Controllers.Schemas; using Iceshrimp.Backend.Core.Configuration; using Microsoft.Extensions.Options; @@ -24,25 +26,35 @@ public class ErrorHandlerMiddleware(IOptions options, IL var logger = loggerFactory.CreateLogger(type); var verbosity = options.Value.ExceptionVerbosity; + var isMastodon = ctx.GetEndpoint()?.Metadata.GetMetadata() != null; + if (e is GracefulException ce) { if (ce.StatusCode == HttpStatusCode.Accepted) { ctx.Response.StatusCode = (int)ce.StatusCode; await ctx.Response.CompleteAsync(); return; } - + if (verbosity > ExceptionVerbosity.Basic && ce.OverrideBasic) verbosity = ExceptionVerbosity.Basic; - ctx.Response.StatusCode = (int)ce.StatusCode; - await ctx.Response.WriteAsJsonAsync(new ErrorResponse { - StatusCode = ctx.Response.StatusCode, - Error = verbosity >= ExceptionVerbosity.Basic ? ce.Error : ce.StatusCode.ToString(), - Message = verbosity >= ExceptionVerbosity.Basic ? ce.Message : null, - Details = verbosity == ExceptionVerbosity.Full ? ce.Details : null, - Source = verbosity == ExceptionVerbosity.Full ? type : null, - RequestId = ctx.TraceIdentifier - }); + ctx.Response.StatusCode = (int)ce.StatusCode; + ctx.Response.Headers.RequestId = ctx.TraceIdentifier; + + if (isMastodon) + await ctx.Response.WriteAsJsonAsync(new MastodonErrorResponse { + Error = verbosity >= ExceptionVerbosity.Basic ? ce.Message : ce.StatusCode.ToString(), + Description = verbosity >= ExceptionVerbosity.Basic ? ce.Details : null + }); + else + await ctx.Response.WriteAsJsonAsync(new ErrorResponse { + StatusCode = ctx.Response.StatusCode, + Error = verbosity >= ExceptionVerbosity.Basic ? ce.Error : ce.StatusCode.ToString(), + Message = verbosity >= ExceptionVerbosity.Basic ? ce.Message : null, + Details = verbosity == ExceptionVerbosity.Full ? ce.Details : null, + Source = verbosity == ExceptionVerbosity.Full ? type : null, + RequestId = ctx.TraceIdentifier + }); if (!ce.SuppressLog) { if (ce.Details != null) @@ -120,7 +132,8 @@ public class GracefulException( } /// - /// This is intended for cases where no error occured, but the request needs to be aborted early (e.g. WebFinger returning 410 Gone) + /// This is intended for cases where no error occured, but the request needs to be aborted early (e.g. WebFinger + /// returning 410 Gone) /// public static GracefulException Accepted(string message) { return new GracefulException(HttpStatusCode.Accepted, message); diff --git a/Iceshrimp.Backend/Core/Middleware/OauthAuthenticationMiddleware.cs b/Iceshrimp.Backend/Core/Middleware/OauthAuthenticationMiddleware.cs deleted file mode 100644 index 8f4107b0..00000000 --- a/Iceshrimp.Backend/Core/Middleware/OauthAuthenticationMiddleware.cs +++ /dev/null @@ -1,67 +0,0 @@ -using Iceshrimp.Backend.Core.Database; -using Iceshrimp.Backend.Core.Database.Tables; -using Iceshrimp.Backend.Core.Helpers; -using Microsoft.AspNetCore.Http.Features; -using Microsoft.EntityFrameworkCore; - -namespace Iceshrimp.Backend.Core.Middleware; - -public class OauthAuthenticationMiddleware(DatabaseContext db) : IMiddleware { - public async Task InvokeAsync(HttpContext ctx, RequestDelegate next) { - var endpoint = ctx.Features.Get()?.Endpoint; - var attribute = endpoint?.Metadata.GetMetadata(); - - if (attribute != null) { - var request = ctx.Request; - var header = request.Headers.Authorization.ToString(); - if (!header.ToLowerInvariant().StartsWith("bearer ")) { - await next(ctx); - return; - } - - header = header[7..]; - var token = await db.OauthTokens - .Include(p => p.User) - .ThenInclude(p => p.UserProfile) - .Include(p => p.App) - .FirstOrDefaultAsync(p => p.Token == header && p.Active); - - if (token == null) { - await next(ctx); - return; - } - - if (attribute.Scopes.Length > 0 && - attribute.Scopes.Except(MastodonOauthHelpers.ExpandScopes(token.Scopes)).Any()) { - await next(ctx); - return; - } - - ctx.SetOauthToken(token); - } - - await next(ctx); - } -} - -public class AuthenticateOauthAttribute(params string[] scopes) : Attribute { - public readonly string[] Scopes = scopes; -} - -public static partial class HttpContextExtensions { - private const string MastodonKey = "masto-session"; - - internal static void SetOauthToken(this HttpContext ctx, OauthToken session) { - ctx.Items.Add(MastodonKey, session); - } - - public static OauthToken? GetOauthToken(this HttpContext ctx) { - ctx.Items.TryGetValue(MastodonKey, out var session); - return session as OauthToken; - } - - public static User? GetOauthUser(this HttpContext ctx) { - ctx.Items.TryGetValue(MastodonKey, out var session); - return (session as OauthToken)?.User; - } -} \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Middleware/OauthAuthorizationMiddleware.cs b/Iceshrimp.Backend/Core/Middleware/OauthAuthorizationMiddleware.cs deleted file mode 100644 index e40c543c..00000000 --- a/Iceshrimp.Backend/Core/Middleware/OauthAuthorizationMiddleware.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Iceshrimp.Backend.Core.Helpers; -using Microsoft.AspNetCore.Http.Features; - -namespace Iceshrimp.Backend.Core.Middleware; - -public class OauthAuthorizationMiddleware : IMiddleware { - public async Task InvokeAsync(HttpContext ctx, RequestDelegate next) { - var endpoint = ctx.Features.Get()?.Endpoint; - var attribute = endpoint?.Metadata.GetMetadata(); - - if (attribute != null) { - var token = ctx.GetOauthToken(); - if (token is not { Active: true }) - throw GracefulException.Unauthorized("This method requires an authenticated user"); - if (attribute.Scopes.Length > 0 && - attribute.Scopes.Except(MastodonOauthHelpers.ExpandScopes(token.Scopes)).Any()) - throw GracefulException.Forbidden("This action is outside the authorized scopes"); - } - - await next(ctx); - } -} - -public class AuthorizeOauthAttribute(params string[] scopes) : Attribute { - public readonly string[] Scopes = scopes; -} \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Middleware/RequestBufferingMiddleware.cs b/Iceshrimp.Backend/Core/Middleware/RequestBufferingMiddleware.cs index 06ef583a..815acabf 100644 --- a/Iceshrimp.Backend/Core/Middleware/RequestBufferingMiddleware.cs +++ b/Iceshrimp.Backend/Core/Middleware/RequestBufferingMiddleware.cs @@ -1,14 +1,9 @@ -using Microsoft.AspNetCore.Http.Features; - namespace Iceshrimp.Backend.Core.Middleware; public class RequestBufferingMiddleware : IMiddleware { public async Task InvokeAsync(HttpContext ctx, RequestDelegate next) { - var endpoint = ctx.Features.Get()?.Endpoint; - var attribute = endpoint?.Metadata.GetMetadata(); - + var attribute = ctx.GetEndpoint()?.Metadata.GetMetadata(); if (attribute != null) ctx.Request.EnableBuffering(attribute.MaxLength); - await next(ctx); } }