From 3f6d54449a9e970f04483ccf30951207b9664e1a Mon Sep 17 00:00:00 2001 From: Jake Mannens Date: Fri, 19 Jun 2026 00:34:17 +1000 Subject: Added basic infrastructure for JWT authentication --- Controllers/ApiLoginController.cs | 46 +++++++++++++++++++++++++++++++++++++++ Enum.cs | 5 +++++ Program.cs | 46 +++++++++++++++++++++++++++++++++++++++ Server.csproj | 3 +++ 4 files changed, 100 insertions(+) create mode 100644 Controllers/ApiLoginController.cs create mode 100644 Enum.cs diff --git a/Controllers/ApiLoginController.cs b/Controllers/ApiLoginController.cs new file mode 100644 index 0000000..fdf9bae --- /dev/null +++ b/Controllers/ApiLoginController.cs @@ -0,0 +1,46 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Security.Cryptography; + +namespace HyperBooru.Controllers; + +[ApiController] +[Route("/api/auth")] +public class ApiLoginController : Controller { + private readonly RSA rsa; + + public ApiLoginController(RSA rsa) => + this.rsa = rsa; + + [HttpPost] + public IActionResult Login([FromBody] LoginRequest request) { + var claims = new[] { + new Claim(ClaimTypes.Name, request.Username), + // TODO: Populate with the user's actual GUID + new Claim("uid", Guid.Empty.ToString().ToLower()), + new Claim("nsfw", request.NsfwClaim.ToString().ToLower()) + }; + + var creds = new SigningCredentials( + new RsaSecurityKey(rsa), + SecurityAlgorithms.RsaSha256); + + var token = new JwtSecurityToken( + claims: claims, + expires: DateTime.UtcNow.AddDays(30), + signingCredentials: creds); + + var jwt = new JwtSecurityTokenHandler().WriteToken(token); + + return Ok(new { token = jwt }); + } + + public record LoginRequest { + public required string Username { get; set; } + public required string Password { get; set; } + + public bool NsfwClaim { get; set; } = false; + } +} diff --git a/Enum.cs b/Enum.cs new file mode 100644 index 0000000..20e384c --- /dev/null +++ b/Enum.cs @@ -0,0 +1,5 @@ +namespace HyperBooru.Server; + +public static class AuthorizationPolicy { + public const string NsfwPolicy = "IsNsfw"; +} diff --git a/Program.cs b/Program.cs index 1694587..2c56209 100644 --- a/Program.cs +++ b/Program.cs @@ -1,8 +1,11 @@ using HyperBooru.Server.Components; using HyperBooru.Services; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Http.Json; using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using System.Security.Cryptography; using System.Text.Json.Serialization; namespace HyperBooru.Server; @@ -39,6 +42,47 @@ public class Program { .GetRequiredService() .KeyPath)); + // Load our persistently-stored JWT signing key + builder.Services.AddSingleton(sp => { + var keyPath = Path.Combine( + sp.GetRequiredService().KeyPath, + "jwt_key"); + + var protector = sp.GetRequiredService() + .CreateProtector("jwt-signing-key"); + + try { + var unprotected = protector.Unprotect(File.ReadAllBytes(keyPath)); + var rsa = RSA.Create(); + rsa.ImportRSAPrivateKey(unprotected, out var _); + return rsa; + } catch { + var rsa = RSA.Create(4096); + var privKey = rsa.ExportRSAPrivateKey(); + File.WriteAllBytes(keyPath, protector.Protect(privKey)); + return rsa; + } + }); + + // Configure JWT token-based authentication + builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(o => { + var rsa = builder.Services.BuildServiceProvider().GetRequiredService(); + o.TokenValidationParameters = new TokenValidationParameters { + ValidateIssuer = false, + ValidateAudience = false, + ValidateIssuerSigningKey = true, + IssuerSigningKey = new RsaSecurityKey(rsa) + }; + }); + + // Configure custom authorization policies + builder.Services.AddAuthorization(o => { + o.AddPolicy(AuthorizationPolicy.NsfwPolicy, p => { + p.RequireClaim("nsfw", "true"); + }); + }); + var app = builder.Build(); // Ensure database is created and it's schema is up to date @@ -52,6 +96,8 @@ public class Program { app.UseExceptionHandler("/Error"); } + app.UseAuthentication(); + app.UseAuthorization(); app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true); app.UseHsts(); app.UseHttpsRedirection(); diff --git a/Server.csproj b/Server.csproj index 5188548..eb636a3 100644 --- a/Server.csproj +++ b/Server.csproj @@ -38,6 +38,7 @@ + @@ -45,10 +46,12 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + -- cgit v1.3