v0.6 prerelease commit with some minor fixes
Release CI / build-and-upload-mod (push) Canceled after 0s
Release CI / build-and-upload-mod (release) Canceled after 0s
Java CI / build (push) Successful in 4m54s
.NET+Docker CI/CD / Unit and Integration tests (push) Successful in 44s
.NET+Docker CI/CD / Push Docker image to ghcr.io (push) Successful in 1m45s

Signed-off-by: Dmitri Shimanski <yawaflua@perseverance.yawaflua.tech>
This commit is contained in:
Dmitrii `yawaflua` Shimanskii
2026-07-21 03:35:52 +03:00
committed by Dmitri Shimanski
parent b1bc688d83
commit d41cd8f079
37 changed files with 1208 additions and 703 deletions
@@ -3,7 +3,9 @@ using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Authentication;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using SpMega.Backend.Persistent.Database;
using SpMega.Backend.Services;
namespace SpMega.Backend.Authetication;
@@ -11,28 +13,39 @@ public class JwtAuthHandler(
IOptionsMonitor<JwtAuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
AppDbContext dbContext) : AuthenticationHandler<JwtAuthenticationSchemeOptions>(options, logger, encoder)
AppDbContext dbContext,
TokenService tokenService) : AuthenticationHandler<JwtAuthenticationSchemeOptions>(options, logger, encoder)
{
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!Request.Headers.ContainsKey("Authorization") || !Request.Headers.Authorization.ToString().StartsWith("Bearer "))
return AuthenticateResult.NoResult();
var token = Request.Headers.Authorization.ToString().Replace("Bearer ", "");
if (string.IsNullOrEmpty(token))
var authorization = Request.Headers.Authorization.ToString();
if (string.IsNullOrWhiteSpace(authorization))
{
return AuthenticateResult.Fail("Invalid authorization header format");
return AuthenticateResult.NoResult();
}
const string bearerPrefix = "Bearer ";
if (!authorization.StartsWith(bearerPrefix, StringComparison.OrdinalIgnoreCase))
{
return AuthenticateResult.Fail("Invalid authorization header format.");
}
var token = authorization[bearerPrefix.Length..].Trim();
if (string.IsNullOrWhiteSpace(token))
{
return AuthenticateResult.Fail("Invalid authorization header format.");
}
try
{
var user = await dbContext.Users.FirstOrDefaultAsync(k => k.Token == token);
var validatedToken = tokenService.ValidateAccessToken(token);
var user = await dbContext.Users.FirstOrDefaultAsync(
candidate => candidate.Id == validatedToken.UserId && !candidate.IsDeleted,
Context.RequestAborted);
if (user == null)
if (user == null || !string.Equals(user.Username, validatedToken.UserName, StringComparison.Ordinal))
{
return AuthenticateResult.Fail("Invalid or expired token");
return AuthenticateResult.Fail("Invalid token subject.");
}
var claims = new List<Claim>
@@ -40,25 +53,39 @@ public class JwtAuthHandler(
new(ClaimTypes.NameIdentifier, user.Id.ToString()),
new(ClaimTypes.Name, user.Username),
new("UserId", user.Id.ToString()),
new("Token", token),
new(ClaimTypes.Role, "default")
};
var claimsIdentity = new ClaimsIdentity(claims, Scheme.Name);
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, Scheme.Name));
Context.Items["User"] = user;
Context.Items["@me"] = user;
Context.Items["user"] = user;
Context.Items[TokenService.HttpContextItemKey] = validatedToken;
var ticket = new AuthenticationTicket(claimsPrincipal, Scheme.Name);
return AuthenticateResult.Success(ticket);
return AuthenticateResult.Success(new AuthenticationTicket(principal, Scheme.Name));
}
catch (Exception ex)
catch (SecurityTokenException exception)
{
Logger.LogError(ex, "Error during JWT authentication");
return AuthenticateResult.Fail("Authentication error occurred");
Logger.LogWarning("JWT authentication rejected: {Reason}", exception.Message);
return AuthenticateResult.Fail("Invalid or expired token.");
}
catch (Exception exception)
{
Logger.LogError(exception, "Error during JWT authentication");
return AuthenticateResult.Fail("Authentication error occurred.");
}
}
protected override Task HandleChallengeAsync(AuthenticationProperties properties)
{
Response.StatusCode = StatusCodes.Status401Unauthorized;
Response.ContentType = "application/json";
Response.Headers.WWWAuthenticate = $"{Scheme.Name} error=\"invalid_token\"";
Response.Headers["X-SPMega-Reauthenticate"] = "true";
return Response.WriteAsJsonAsync(new
{
code = "reauthentication_required",
message = "The access token is missing, invalid, expired, or older than two days."
});
}
}