using System.Text; using TinfoilVibeServer.Authentication; namespace TinfoilVibeServer.Middleware; /// /// Minimal Basic‑Auth middleware that also checks UID, failure counters and a blacklist. /// public sealed class BasicAuthMiddleware { private readonly RequestDelegate _next; public BasicAuthMiddleware(RequestDelegate next) { _next = next; } public async Task InvokeAsync(HttpContext context, AuthStore store, ILogger logger) { var ip = context.Connection.RemoteIpAddress?.ToString() ?? "unknown"; // 1) IP blacklist if (store.IsBlacklisted(ip)) { logger.LogWarning("Blocked request from blacklisted IP {IP}", ip); context.Response.StatusCode = StatusCodes.Status403Forbidden; await context.Response.WriteAsync("Forbidden"); return; } // 2) Authorization header if (!context.Request.Headers.TryGetValue("Authorization", out var authHeaders)) { Challenge(context); return; } var authHeader = authHeaders.FirstOrDefault() ?? ""; if (!authHeader.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase)) { Challenge(context); return; } string decoded; try { var b64 = authHeader[6..].Trim(); decoded = Encoding.UTF8.GetString(Convert.FromBase64String(b64)); } catch { Challenge(context); return; } var parts = decoded.Split(':', 2); if (parts.Length != 2) { Challenge(context); return; } var username = parts[0]; var password = parts[1]; // 3) UID header (optional) int? uid = null; if (context.Request.Headers.TryGetValue("UID", out var uidHeader)) { if (int.TryParse(uidHeader.ToString(), out var parsedUid)) uid = parsedUid; } // 4) Validate if (!store.TryValidate(username, password, uid, ip, out var error)) { logger.LogWarning("Auth failed for user {User} from {IP}: {Error}", username, ip, error); context.Response.StatusCode = StatusCodes.Status401Unauthorized; context.Response.Headers.Add("WWW-Authenticate", "Basic realm=\"FileSnapshot\""); await context.Response.WriteAsync(error ?? "Unauthorized"); return; } // Authentication succeeded – attach username for downstream handlers if needed context.Items["User"] = username; await _next(context); } private static void Challenge(HttpContext ctx) { ctx.Response.StatusCode = StatusCodes.Status401Unauthorized; ctx.Response.Headers.Add("WWW-Authenticate", "Basic realm=\"FileSnapshot\""); } }