diff options
| author | Jake Mannens <jake@asger.xyz> | 2026-06-19 00:34:17 +1000 |
|---|---|---|
| committer | Jake Mannens <jake@asger.xyz> | 2026-06-25 02:43:49 +1000 |
| commit | 3f6d54449a9e970f04483ccf30951207b9664e1a (patch) | |
| tree | 5cff1a3ab27115c6f58b54542c232bdbf5b1c2fa | |
| parent | 8539f1142e9152cf9d285fa8be2738b48265a736 (diff) | |
Added basic infrastructure for JWT authentication
| -rw-r--r-- | Controllers/ApiLoginController.cs | 46 | ||||
| -rw-r--r-- | Enum.cs | 5 | ||||
| -rw-r--r-- | Program.cs | 46 | ||||
| -rw-r--r-- | Server.csproj | 3 |
4 files changed, 100 insertions, 0 deletions
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; + } +} @@ -0,0 +1,5 @@ +namespace HyperBooru.Server; + +public static class AuthorizationPolicy { + public const string NsfwPolicy = "IsNsfw"; +} @@ -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<IConfigService>() .KeyPath)); + // Load our persistently-stored JWT signing key + builder.Services.AddSingleton<RSA>(sp => { + var keyPath = Path.Combine( + sp.GetRequiredService<IConfigService>().KeyPath, + "jwt_key"); + + var protector = sp.GetRequiredService<IDataProtectionProvider>() + .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<RSA>(); + 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 @@ </ItemGroup> <ItemGroup> + <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.9" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.8" /> <PackageReference Include="Magick.NET-Q16-AnyCPU" Version="14.14.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.8" /> @@ -45,10 +46,12 @@ <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> + <PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.19.1" /> <PackageReference Include="Mime-Detective" Version="25.8.1" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.2" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="10.2.1" /> <PackageReference Include="System.Drawing.Common" Version="10.0.8" /> + <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.19.1" /> <PackageReference Include="Tesseract" Version="5.2.0" /> </ItemGroup> |
