Add basic authentication system
This commit is contained in:
parent
6fde3ede7c
commit
e755f9f96f
7 changed files with 94 additions and 4 deletions
|
@ -4,6 +4,7 @@ using Iceshrimp.Backend.Controllers.Schemas;
|
|||
using Iceshrimp.Backend.Core.Database;
|
||||
using Iceshrimp.Backend.Core.Database.Tables;
|
||||
using Iceshrimp.Backend.Core.Helpers;
|
||||
using Iceshrimp.Backend.Core.Middleware;
|
||||
using Iceshrimp.Backend.Core.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
|
@ -18,9 +19,27 @@ namespace Iceshrimp.Backend.Controllers;
|
|||
[Route("/api/iceshrimp/v1/auth")]
|
||||
public class AuthController(DatabaseContext db, UserService userSvc) : Controller {
|
||||
[HttpGet]
|
||||
[Authentication(false)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(AuthResponse))]
|
||||
public IActionResult GetAuthStatus() {
|
||||
return new StatusCodeResult((int)HttpStatusCode.NotImplemented);
|
||||
var session = Request.HttpContext.GetSession();
|
||||
|
||||
if (session == null) {
|
||||
return Ok(new AuthResponse {
|
||||
Status = AuthStatusEnum.Guest
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(new AuthResponse {
|
||||
Status = session.Active ? AuthStatusEnum.Authenticated : AuthStatusEnum.TwoFactor,
|
||||
Token = session.Token,
|
||||
User = new UserResponse {
|
||||
Username = session.User.Username,
|
||||
Id = session.User.Id,
|
||||
AvatarUrl = session.User.AvatarUrl,
|
||||
BannerUrl = session.User.BannerUrl
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
|
@ -49,6 +68,8 @@ public class AuthController(DatabaseContext db, UserService userSvc) : Controlle
|
|||
});
|
||||
|
||||
var session = res.Entity;
|
||||
await db.AddAsync(session);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return Ok(new AuthResponse {
|
||||
Status = session.Active ? AuthStatusEnum.Authenticated : AuthStatusEnum.TwoFactor,
|
||||
|
@ -71,7 +92,6 @@ public class AuthController(DatabaseContext db, UserService userSvc) : Controlle
|
|||
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(ErrorResponse))]
|
||||
public async Task<IActionResult> Register([FromBody] RegistrationRequest request) {
|
||||
//TODO: captcha support
|
||||
//TODO: invite support
|
||||
|
||||
await userSvc.CreateLocalUser(request.Username, request.Password, request.Invite);
|
||||
return await Login(request);
|
||||
|
|
|
@ -16,6 +16,6 @@ public enum AuthStatusEnum {
|
|||
|
||||
public class AuthResponse {
|
||||
[J("status")] public required AuthStatusEnum Status { get; set; }
|
||||
[J("token")] public required string? Token { get; set; }
|
||||
[J("user")] public required UserResponse? User { get; set; }
|
||||
[J("user")] public UserResponse? User { get; set; }
|
||||
[J("token")] public string? Token { get; set; }
|
||||
}
|
|
@ -30,6 +30,7 @@ public static class ServiceExtensions {
|
|||
services.AddScoped<ActivityHandlerService>();
|
||||
services.AddScoped<WebFingerService>();
|
||||
services.AddScoped<AuthorizedFetchMiddleware>();
|
||||
services.AddScoped<AuthenticationMiddleware>();
|
||||
|
||||
// Singleton = instantiated once across application lifetime
|
||||
services.AddSingleton<HttpClient>();
|
||||
|
|
|
@ -11,6 +11,7 @@ public static class WebApplicationExtensions {
|
|||
// Caution: make sure these are in the correct order
|
||||
return app.UseMiddleware<ErrorHandlerMiddleware>()
|
||||
.UseMiddleware<RequestBufferingMiddleware>()
|
||||
.UseMiddleware<AuthenticationMiddleware>()
|
||||
.UseMiddleware<AuthorizedFetchMiddleware>();
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using Iceshrimp.Backend.Core.Database;
|
||||
using Iceshrimp.Backend.Core.Database.Tables;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Iceshrimp.Backend.Core.Middleware;
|
||||
|
||||
public class AuthenticationMiddleware(
|
||||
[SuppressMessage("ReSharper", "SuggestBaseTypeForParameterInConstructor")]
|
||||
DatabaseContext db
|
||||
) : IMiddleware {
|
||||
public async Task InvokeAsync(HttpContext ctx, RequestDelegate next) {
|
||||
var endpoint = ctx.Features.Get<IEndpointFeature>()?.Endpoint;
|
||||
var attribute = endpoint?.Metadata.GetMetadata<AuthenticationAttribute>();
|
||||
|
||||
if (attribute != null) {
|
||||
var request = ctx.Request;
|
||||
var header = request.Headers.Authorization.ToString();
|
||||
if (!header.ToLowerInvariant().StartsWith("bearer ")) {
|
||||
if (attribute.Required)
|
||||
throw GracefulException.Unauthorized("Missing bearer token in authorization header");
|
||||
await next(ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
var token = header[7..];
|
||||
var session = await db.Sessions.Include(p => p.User).FirstOrDefaultAsync(p => p.Token == token);
|
||||
if (session == null) {
|
||||
if (attribute.Required)
|
||||
throw GracefulException.Forbidden("Bearer token is invalid");
|
||||
await next(ctx);
|
||||
return;
|
||||
}
|
||||
ctx.SetSession(session);
|
||||
}
|
||||
|
||||
await next(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
public class AuthenticationAttribute(bool required = true) : Attribute {
|
||||
public bool Required { get; } = required;
|
||||
}
|
||||
|
||||
public static class HttpContextExtensions {
|
||||
private const string Key = "session";
|
||||
|
||||
internal static void SetSession(this HttpContext ctx, Session session) {
|
||||
ctx.Items.Add(Key, session);
|
||||
}
|
||||
|
||||
public static Session? GetSession(this HttpContext ctx) {
|
||||
ctx.Items.TryGetValue(Key, out var session);
|
||||
return session as Session;
|
||||
}
|
||||
|
||||
public static User? GetUser(this HttpContext ctx) {
|
||||
ctx.Items.TryGetValue(Key, out var session);
|
||||
return (session as Session)?.User;
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net;
|
||||
using Iceshrimp.Backend.Core.Configuration;
|
||||
using Iceshrimp.Backend.Core.Database;
|
||||
|
@ -11,6 +12,7 @@ using Microsoft.Extensions.Options;
|
|||
namespace Iceshrimp.Backend.Core.Middleware;
|
||||
|
||||
public class AuthorizedFetchMiddleware(
|
||||
[SuppressMessage("ReSharper", "SuggestBaseTypeForParameterInConstructor")]
|
||||
IOptionsSnapshot<Config.SecuritySection> config,
|
||||
DatabaseContext db,
|
||||
UserResolver userResolver,
|
||||
|
|
|
@ -78,6 +78,10 @@ public class GracefulException(HttpStatusCode statusCode, string error, string m
|
|||
return new GracefulException(HttpStatusCode.Forbidden, message, details);
|
||||
}
|
||||
|
||||
public static GracefulException Unauthorized(string message, string? details = null) {
|
||||
return new GracefulException(HttpStatusCode.Unauthorized, message, details);
|
||||
}
|
||||
|
||||
public static GracefulException NotFound(string message, string? details = null) {
|
||||
return new GracefulException(HttpStatusCode.NotFound, message, details);
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue