Mastodon client API foundations
This commit is contained in:
parent
3f333b0c5d
commit
cec4abd841
13 changed files with 434 additions and 32 deletions
|
@ -1,3 +1,9 @@
|
||||||
|
using Iceshrimp.Backend.Controllers.Mastodon.Schemas;
|
||||||
|
using Iceshrimp.Backend.Core.Database;
|
||||||
|
using Iceshrimp.Backend.Core.Database.Tables;
|
||||||
|
using Iceshrimp.Backend.Core.Extensions;
|
||||||
|
using Iceshrimp.Backend.Core.Helpers;
|
||||||
|
using Iceshrimp.Backend.Core.Middleware;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.RateLimiting;
|
using Microsoft.AspNetCore.RateLimiting;
|
||||||
|
|
||||||
|
@ -8,4 +14,55 @@ namespace Iceshrimp.Backend.Controllers.Mastodon;
|
||||||
[EnableRateLimiting("sliding")]
|
[EnableRateLimiting("sliding")]
|
||||||
[Produces("application/json")]
|
[Produces("application/json")]
|
||||||
[Route("/api/v1")]
|
[Route("/api/v1")]
|
||||||
public class MastodonAuthController : Controller { }
|
public class MastodonAuthController(DatabaseContext db) : Controller {
|
||||||
|
[AuthenticateOauth]
|
||||||
|
[HttpGet("verify_credentials")]
|
||||||
|
[Produces("application/json")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(MastodonAuth.VerifyCredentialsResponse))]
|
||||||
|
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(MastodonErrorResponse))]
|
||||||
|
public IActionResult VerifyCredentials() {
|
||||||
|
var token = HttpContext.GetOauthToken();
|
||||||
|
if (token == null) throw GracefulException.Unauthorized("The access token is invalid");
|
||||||
|
|
||||||
|
var res = new MastodonAuth.VerifyCredentialsResponse {
|
||||||
|
App = token.App,
|
||||||
|
VapidKey = null //FIXME
|
||||||
|
};
|
||||||
|
|
||||||
|
return Ok(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("apps")]
|
||||||
|
[Consumes("application/json", "application/x-www-form-urlencoded", "multipart/form-data")]
|
||||||
|
[Produces("application/json")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(MastodonAuth.RegisterAppResponse))]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(MastodonErrorResponse))]
|
||||||
|
public async Task<IActionResult> RegisterApp([FromHybrid] MastodonAuth.RegisterAppRequest request) {
|
||||||
|
if (request.RedirectUris.Count == 0)
|
||||||
|
throw GracefulException.BadRequest("Invalid redirect_uris parameter");
|
||||||
|
|
||||||
|
if (request.RedirectUris.Any(p => !MastodonOauthHelpers.ValidateRedirectUri(p)))
|
||||||
|
throw GracefulException.BadRequest("redirect_uris parameter contains invalid protocols");
|
||||||
|
|
||||||
|
var app = new OauthApp {
|
||||||
|
Id = IdHelpers.GenerateSlowflakeId(),
|
||||||
|
ClientId = CryptographyHelpers.GenerateRandomString(32),
|
||||||
|
ClientSecret = CryptographyHelpers.GenerateRandomString(32),
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
Name = request.ClientName,
|
||||||
|
Website = request.Website,
|
||||||
|
Scopes = request.Scopes,
|
||||||
|
RedirectUris = request.RedirectUris,
|
||||||
|
};
|
||||||
|
|
||||||
|
await db.AddAsync(app);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
var res = new MastodonAuth.RegisterAppResponse {
|
||||||
|
App = app,
|
||||||
|
VapidKey = null //FIXME
|
||||||
|
};
|
||||||
|
|
||||||
|
return Ok(res);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
using Iceshrimp.Backend.Core.Database.Tables;
|
||||||
|
using Iceshrimp.Backend.Core.Helpers;
|
||||||
|
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
||||||
|
using JR = System.Text.Json.Serialization.JsonRequiredAttribute;
|
||||||
|
using JC = System.Text.Json.Serialization.JsonConverterAttribute;
|
||||||
|
using B = Microsoft.AspNetCore.Mvc.BindPropertyAttribute;
|
||||||
|
|
||||||
|
namespace Iceshrimp.Backend.Controllers.Mastodon.Schemas;
|
||||||
|
|
||||||
|
public abstract class MastodonAuth {
|
||||||
|
public class VerifyCredentialsResponse {
|
||||||
|
public required OauthApp App;
|
||||||
|
|
||||||
|
[J("name")] public string Name => App.Name;
|
||||||
|
[J("website")] public string? Website => App.Website;
|
||||||
|
|
||||||
|
[J("vapid_key")] public required string? VapidKey { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RegisterAppRequest {
|
||||||
|
private List<string> _scopes = ["read"];
|
||||||
|
public List<string> RedirectUris = [];
|
||||||
|
|
||||||
|
[B(Name = "scopes")]
|
||||||
|
[J("scopes")]
|
||||||
|
[JC(typeof(EnsureArrayConverter))]
|
||||||
|
public List<string> Scopes {
|
||||||
|
get => _scopes;
|
||||||
|
set => _scopes = value.Count == 1
|
||||||
|
? value[0].Split(' ').ToList()
|
||||||
|
: value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[B(Name = "client_name")]
|
||||||
|
[J("client_name")]
|
||||||
|
[JR]
|
||||||
|
public string ClientName { get; set; } = null!;
|
||||||
|
|
||||||
|
[B(Name = "website")] [J("website")] public string? Website { get; set; }
|
||||||
|
|
||||||
|
[B(Name = "redirect_uris")]
|
||||||
|
[J("redirect_uris")]
|
||||||
|
public string RedirectUrisInternal {
|
||||||
|
set => RedirectUris = value.Split('\n').ToList();
|
||||||
|
get => string.Join('\n', RedirectUris);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RegisterAppResponse {
|
||||||
|
public required OauthApp App;
|
||||||
|
|
||||||
|
[J("id")] public string Id => App.Id;
|
||||||
|
[J("name")] public string Name => App.Name;
|
||||||
|
[J("website")] public string? Website => App.Website;
|
||||||
|
[J("client_id")] public string ClientId => App.ClientId;
|
||||||
|
[J("client_secret")] public string ClientSecret => App.ClientSecret;
|
||||||
|
[J("redirect_uri")] public string RedirectUri => string.Join("\n", App.RedirectUris);
|
||||||
|
|
||||||
|
[J("vapid_key")] public required string? VapidKey { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
||||||
|
|
||||||
|
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; }
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
|
||||||
|
|
||||||
|
namespace Iceshrimp.Backend.Core.Extensions;
|
||||||
|
|
||||||
|
public static class ModelBinderProviderExtensions {
|
||||||
|
public static void AddHybridBindingProvider(this IList<IModelBinderProvider> providers) {
|
||||||
|
if (providers.Single(provider => provider.GetType() == typeof(BodyModelBinderProvider)) is not
|
||||||
|
BodyModelBinderProvider bodyProvider ||
|
||||||
|
providers.Single(provider => provider.GetType() == typeof(ComplexObjectModelBinderProvider)) is not
|
||||||
|
ComplexObjectModelBinderProvider complexProvider)
|
||||||
|
throw new Exception("Failed to set up hybrid model binding provider");
|
||||||
|
|
||||||
|
var hybridProvider = new HybridModelBinderProvider(bodyProvider, complexProvider);
|
||||||
|
|
||||||
|
providers.Insert(0, hybridProvider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class HybridModelBinderProvider(
|
||||||
|
IModelBinderProvider bodyProvider,
|
||||||
|
IModelBinderProvider complexProvider) : IModelBinderProvider {
|
||||||
|
public IModelBinder? GetBinder(ModelBinderProviderContext context) {
|
||||||
|
if (context.BindingInfo.BindingSource == null) return null;
|
||||||
|
if (!context.BindingInfo.BindingSource.CanAcceptDataFrom(HybridBindingSource.Hybrid)) return null;
|
||||||
|
|
||||||
|
context.BindingInfo.BindingSource = BindingSource.Body;
|
||||||
|
var bodyBinder = bodyProvider.GetBinder(context);
|
||||||
|
context.BindingInfo.BindingSource = BindingSource.ModelBinding;
|
||||||
|
var complexBinder = complexProvider.GetBinder(context);
|
||||||
|
|
||||||
|
return new HybridModelBinder(bodyBinder, complexBinder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class HybridModelBinder(
|
||||||
|
IModelBinder? bodyBinder,
|
||||||
|
IModelBinder? complexBinder
|
||||||
|
) : IModelBinder {
|
||||||
|
public async Task BindModelAsync(ModelBindingContext bindingContext) {
|
||||||
|
if (bodyBinder != null && bindingContext is
|
||||||
|
{ IsTopLevelObject: true, HttpContext.Request.HasFormContentType: false }) {
|
||||||
|
bindingContext.BindingSource = BindingSource.Body;
|
||||||
|
await bodyBinder.BindModelAsync(bindingContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (complexBinder != null && !bindingContext.Result.IsModelSet) {
|
||||||
|
bindingContext.BindingSource = BindingSource.ModelBinding;
|
||||||
|
await complexBinder.BindModelAsync(bindingContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bindingContext.Result.IsModelSet) bindingContext.Model = bindingContext.Result.Model;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property)]
|
||||||
|
public class FromHybridAttribute : Attribute, IBindingSourceMetadata {
|
||||||
|
public BindingSource BindingSource => HybridBindingSource.Hybrid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class HybridBindingSource() : BindingSource("Hybrid", "Hybrid", true, true) {
|
||||||
|
public static readonly HybridBindingSource Hybrid = new();
|
||||||
|
|
||||||
|
public override bool CanAcceptDataFrom(BindingSource bindingSource) {
|
||||||
|
return bindingSource == this;
|
||||||
|
}
|
||||||
|
}
|
|
@ -107,11 +107,16 @@ public static class ServiceExtensions {
|
||||||
Type = SecuritySchemeType.Http,
|
Type = SecuritySchemeType.Http,
|
||||||
Scheme = "bearer"
|
Scheme = "bearer"
|
||||||
});
|
});
|
||||||
|
options.AddSecurityDefinition("mastodon", new OpenApiSecurityScheme {
|
||||||
|
Name = "Authorization token",
|
||||||
|
In = ParameterLocation.Header,
|
||||||
|
Type = SecuritySchemeType.Http,
|
||||||
|
Scheme = "bearer"
|
||||||
|
});
|
||||||
options.OperationFilter<AuthorizeCheckOperationFilter>();
|
options.OperationFilter<AuthorizeCheckOperationFilter>();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static void AddSlidingWindowRateLimiter(this IServiceCollection services) {
|
public static void AddSlidingWindowRateLimiter(this IServiceCollection services) {
|
||||||
//TODO: separate limiter for authenticated users, partitioned by user id
|
//TODO: separate limiter for authenticated users, partitioned by user id
|
||||||
//TODO: ipv6 /64 subnet buckets
|
//TODO: ipv6 /64 subnet buckets
|
||||||
|
@ -153,16 +158,22 @@ public static class ServiceExtensions {
|
||||||
return;
|
return;
|
||||||
|
|
||||||
//TODO: separate admin & user authorize attributes
|
//TODO: separate admin & user authorize attributes
|
||||||
var hasAuthorize = context.MethodInfo.DeclaringType.GetCustomAttributes(true)
|
var hasAuthenticate = context.MethodInfo.DeclaringType.GetCustomAttributes(true)
|
||||||
.OfType<AuthenticateAttribute>().Any() ||
|
.OfType<AuthenticateAttribute>().Any() ||
|
||||||
context.MethodInfo.GetCustomAttributes(true)
|
context.MethodInfo.GetCustomAttributes(true)
|
||||||
.OfType<AuthenticateAttribute>().Any();
|
.OfType<AuthenticateAttribute>().Any();
|
||||||
|
|
||||||
if (!hasAuthorize) return;
|
var hasOauthAuthenticate = context.MethodInfo.DeclaringType.GetCustomAttributes(true)
|
||||||
|
.OfType<AuthenticateOauthAttribute>().Any() ||
|
||||||
|
context.MethodInfo.GetCustomAttributes(true)
|
||||||
|
.OfType<AuthenticateOauthAttribute>().Any();
|
||||||
|
|
||||||
|
if (!hasAuthenticate && !hasOauthAuthenticate) return;
|
||||||
|
|
||||||
var schema = new OpenApiSecurityScheme {
|
var schema = new OpenApiSecurityScheme {
|
||||||
Reference = new OpenApiReference {
|
Reference = new OpenApiReference {
|
||||||
Type = ReferenceType.SecurityScheme,
|
Type = ReferenceType.SecurityScheme,
|
||||||
Id = "user"
|
Id = hasAuthenticate ? "user" : "mastodon"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
34
Iceshrimp.Backend/Core/Helpers/JsonConverters.cs
Normal file
34
Iceshrimp.Backend/Core/Helpers/JsonConverters.cs
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Iceshrimp.Backend.Core.Helpers;
|
||||||
|
|
||||||
|
public class EnsureArrayConverter : JsonConverter<List<string>> {
|
||||||
|
public override List<string> Read(ref Utf8JsonReader reader, Type typeToConvert,
|
||||||
|
JsonSerializerOptions options) {
|
||||||
|
if (reader.TokenType == JsonTokenType.StartArray) {
|
||||||
|
var list = new List<string>();
|
||||||
|
reader.Read();
|
||||||
|
|
||||||
|
while (reader.TokenType != JsonTokenType.EndArray) {
|
||||||
|
list.Add(JsonSerializer.Deserialize<string>(ref reader, options) ??
|
||||||
|
throw new InvalidOperationException());
|
||||||
|
reader.Read();
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reader.TokenType == JsonTokenType.String) {
|
||||||
|
var str = JsonSerializer.Deserialize<string>(ref reader, options) ??
|
||||||
|
throw new InvalidOperationException();
|
||||||
|
return [str];
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, List<string> value, JsonSerializerOptions options) {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
74
Iceshrimp.Backend/Core/Helpers/MastodonOauthHelpers.cs
Normal file
74
Iceshrimp.Backend/Core/Helpers/MastodonOauthHelpers.cs
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
using Iceshrimp.Backend.Core.Middleware;
|
||||||
|
|
||||||
|
namespace Iceshrimp.Backend.Core.Helpers;
|
||||||
|
|
||||||
|
public static class MastodonOauthHelpers {
|
||||||
|
private static readonly List<string> ReadScopes = [
|
||||||
|
"read:accounts",
|
||||||
|
"read:blocks",
|
||||||
|
"read:bookmarks",
|
||||||
|
"read:favourites",
|
||||||
|
"read:filters",
|
||||||
|
"read:follows",
|
||||||
|
"read:lists",
|
||||||
|
"read:mutes",
|
||||||
|
"read:notifications",
|
||||||
|
"read:search",
|
||||||
|
"read:statuses"
|
||||||
|
];
|
||||||
|
|
||||||
|
private static readonly List<string> WriteScopes = [
|
||||||
|
"write:accounts",
|
||||||
|
"write:blocks",
|
||||||
|
"write:bookmarks",
|
||||||
|
"write:conversations",
|
||||||
|
"write:favourites",
|
||||||
|
"write:filters",
|
||||||
|
"write:follows",
|
||||||
|
"write:lists",
|
||||||
|
"write:media",
|
||||||
|
"write:mutes",
|
||||||
|
"write:notifications",
|
||||||
|
"write:reports",
|
||||||
|
"write:statuses"
|
||||||
|
];
|
||||||
|
|
||||||
|
private static readonly List<string> FollowScopes = [
|
||||||
|
"read:follows",
|
||||||
|
"read:blocks",
|
||||||
|
"read:mutes",
|
||||||
|
"write:follows",
|
||||||
|
"write:blocks",
|
||||||
|
"write:mutes"
|
||||||
|
];
|
||||||
|
|
||||||
|
public static IEnumerable<string> ExpandScopes(IEnumerable<string> scopes) {
|
||||||
|
var res = new List<string>();
|
||||||
|
foreach (var scope in scopes) {
|
||||||
|
if (scope == "read")
|
||||||
|
res.AddRange(ReadScopes);
|
||||||
|
if (scope == "write")
|
||||||
|
res.AddRange(WriteScopes);
|
||||||
|
if (scope == "follow")
|
||||||
|
res.AddRange(FollowScopes);
|
||||||
|
else {
|
||||||
|
res.Add(scope);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.Distinct();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly List<string> ForbiddenSchemes = ["javascript", "file", "data", "mailto", "tel"];
|
||||||
|
|
||||||
|
public static bool ValidateRedirectUri(string uri) {
|
||||||
|
if (uri == "urn:ietf:wg:oauth:2.0:oob") return true;
|
||||||
|
try {
|
||||||
|
var proto = new Uri(uri).Scheme;
|
||||||
|
return !ForbiddenSchemes.Contains(proto);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,7 +19,7 @@ public class AuthenticationMiddleware(DatabaseContext db) : IMiddleware {
|
||||||
}
|
}
|
||||||
|
|
||||||
var token = header[7..];
|
var token = header[7..];
|
||||||
var session = await db.Sessions.Include(p => p.User).FirstOrDefaultAsync(p => p.Token == token);
|
var session = await db.Sessions.Include(p => p.User).FirstOrDefaultAsync(p => p.Token == token && p.Active);
|
||||||
if (session == null) {
|
if (session == null) {
|
||||||
await next(ctx);
|
await next(ctx);
|
||||||
return;
|
return;
|
||||||
|
@ -34,7 +34,7 @@ public class AuthenticationMiddleware(DatabaseContext db) : IMiddleware {
|
||||||
|
|
||||||
public class AuthenticateAttribute : Attribute;
|
public class AuthenticateAttribute : Attribute;
|
||||||
|
|
||||||
public static class HttpContextExtensions {
|
public static partial class HttpContextExtensions {
|
||||||
private const string Key = "session";
|
private const string Key = "session";
|
||||||
|
|
||||||
internal static void SetSession(this HttpContext ctx, Session session) {
|
internal static void SetSession(this HttpContext ctx, Session session) {
|
||||||
|
|
|
@ -85,6 +85,14 @@ public class GracefulException(HttpStatusCode statusCode, string error, string m
|
||||||
public static GracefulException NotFound(string message, string? details = null) {
|
public static GracefulException NotFound(string message, string? details = null) {
|
||||||
return new GracefulException(HttpStatusCode.NotFound, message, details);
|
return new GracefulException(HttpStatusCode.NotFound, message, details);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static GracefulException BadRequest(string message, string? details = null) {
|
||||||
|
return new GracefulException(HttpStatusCode.BadRequest, message, details);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GracefulException RecordNotFound() {
|
||||||
|
return new GracefulException(HttpStatusCode.NotFound, "Record not found");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ExceptionVerbosity {
|
public enum ExceptionVerbosity {
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
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<IEndpointFeature>()?.Endpoint;
|
||||||
|
var attribute = endpoint?.Metadata.GetMetadata<AuthenticateOauthAttribute>();
|
||||||
|
|
||||||
|
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)
|
||||||
|
.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 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
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<IEndpointFeature>()?.Endpoint;
|
||||||
|
var attribute = endpoint?.Metadata.GetMetadata<AuthorizeOauthAttribute>();
|
||||||
|
|
||||||
|
if (attribute != null)
|
||||||
|
if (ctx.GetOauthToken() is not { Active: true })
|
||||||
|
throw GracefulException.Forbidden("This method requires an authenticated user");
|
||||||
|
|
||||||
|
await next(ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AuthorizeOauthAttribute : Attribute;
|
|
@ -14,41 +14,41 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AngleSharp" Version="1.1.0"/>
|
<PackageReference Include="AngleSharp" Version="1.1.0" />
|
||||||
<PackageReference Include="Asp.Versioning.Http" Version="8.0.0"/>
|
<PackageReference Include="Asp.Versioning.Http" Version="8.0.0" />
|
||||||
<PackageReference Include="cuid.net" Version="5.0.2"/>
|
<PackageReference Include="cuid.net" Version="5.0.2" />
|
||||||
<PackageReference Include="dotNetRdf.Core" Version="3.2.1-dev"/>
|
<PackageReference Include="dotNetRdf.Core" Version="3.2.1-dev" />
|
||||||
<PackageReference Include="Isopoh.Cryptography.Argon2" Version="2.0.0"/>
|
<PackageReference Include="Isopoh.Cryptography.Argon2" Version="2.0.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection.EntityFrameworkCore" Version="8.0.1"/>
|
<PackageReference Include="Microsoft.AspNetCore.DataProtection.EntityFrameworkCore" Version="8.0.1" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.0"/>
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.0"/>
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.1"/>
|
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Ini" Version="8.0.0"/>
|
<PackageReference Include="Microsoft.Extensions.Configuration.Ini" Version="8.0.0" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0"/>
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
|
||||||
<PackageReference Include="protobuf-net" Version="3.2.30"/>
|
<PackageReference Include="protobuf-net" Version="3.2.30" />
|
||||||
<PackageReference Include="StackExchange.Redis" Version="2.7.17"/>
|
<PackageReference Include="StackExchange.Redis" Version="2.7.17" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0"/>
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||||
<PackageReference Include="Vite.AspNetCore" Version="1.11.0"/>
|
<PackageReference Include="Vite.AspNetCore" Version="1.11.0" />
|
||||||
<PackageReference Include="YamlDotNet" Version="13.7.1"/>
|
<PackageReference Include="YamlDotNet" Version="13.7.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<AdditionalFiles Include="Pages\Error.cshtml"/>
|
<AdditionalFiles Include="Pages\Error.cshtml" />
|
||||||
<AdditionalFiles Include="Pages\Shared\_Layout.cshtml"/>
|
<AdditionalFiles Include="Pages\Shared\_Layout.cshtml" />
|
||||||
<AdditionalFiles Include="Pages\_ViewImports.cshtml"/>
|
<AdditionalFiles Include="Pages\_ViewImports.cshtml" />
|
||||||
<AdditionalFiles Include="Pages\_ViewStart.cshtml"/>
|
<AdditionalFiles Include="Pages\_ViewStart.cshtml" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Core\Database\Tables\"/>
|
<Folder Include="Core\Database\Tables\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Content Include="wwwroot\.vite\manifest.json" CopyToPublishDirectory="PreserveNewest"/>
|
<Content Include="wwwroot\.vite\manifest.json" CopyToPublishDirectory="PreserveNewest" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
using Asp.Versioning;
|
using Asp.Versioning;
|
||||||
using Iceshrimp.Backend.Core.Extensions;
|
using Iceshrimp.Backend.Core.Extensions;
|
||||||
using Microsoft.OpenApi.Models;
|
|
||||||
using Swashbuckle.AspNetCore.SwaggerUI;
|
|
||||||
using Vite.AspNetCore.Extensions;
|
using Vite.AspNetCore.Extensions;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
@ -14,7 +12,7 @@ builder.Configuration
|
||||||
.AddIniFile(Environment.GetEnvironmentVariable("ICESHRIMP_CONFIG_OVERRIDES") ?? "configuration.overrides.ini",
|
.AddIniFile(Environment.GetEnvironmentVariable("ICESHRIMP_CONFIG_OVERRIDES") ?? "configuration.overrides.ini",
|
||||||
true, true);
|
true, true);
|
||||||
|
|
||||||
builder.Services.AddControllers()
|
builder.Services.AddControllers(options => { options.ModelBinderProviders.AddHybridBindingProvider(); })
|
||||||
.AddNewtonsoftJson() //TODO: remove once dotNetRdf switches to System.Text.Json
|
.AddNewtonsoftJson() //TODO: remove once dotNetRdf switches to System.Text.Json
|
||||||
.AddMultiFormatter();
|
.AddMultiFormatter();
|
||||||
builder.Services.AddApiVersioning(options => {
|
builder.Services.AddApiVersioning(options => {
|
||||||
|
|
Loading…
Add table
Reference in a new issue