using System.Net; using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text; using Microsoft.AspNetCore.Mvc.Testing; using Wiki.Host.Auth; namespace Wiki.Host.Tests; /// A Secret-like directory: one user per hash format Traefik accepts, split over two keys, plus a /// hidden entry that must be skipped. Every password is the user name. public static class BasicUsers { // From the Traefik BasicAuth docs (htpasswd -m, password "test"); openssl passwd -apr1 agrees. public const string Apr1Line = "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/"; public static readonly string Directory = Write(); private static string Write() { var dir = Path.Combine(Path.GetTempPath(), "wiki-host-tests", $"htpasswd-{Guid.NewGuid():N}"); System.IO.Directory.CreateDirectory(dir); var bcrypt = BCrypt.Net.BCrypt.HashPassword("agent", 4).Replace("$2a$", "$2y$"); var sha = "{SHA}" + Convert.ToBase64String(System.Security.Cryptography.SHA1.HashData(Encoding.UTF8.GetBytes("legacy"))); File.WriteAllLines(Path.Combine(dir, "users"), [Apr1Line, $"agent:{bcrypt}"]); File.WriteAllLines(Path.Combine(dir, "more"), [$"legacy:{sha}"]); File.WriteAllLines(Path.Combine(dir, "..hidden"), ["ghost:{SHA}" + Convert.ToBase64String(System.Security.Cryptography.SHA1.HashData(Encoding.UTF8.GetBytes("ghost")))]); return dir; } } public class BasicAuthTests(WikiHostFactory factory) : IClassFixture { private HttpClient As(string user, string password) { var client = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes($"{user}:{password}"))); return client; } [Fact] public void Apr1MatchesApacheHtpasswd() => Assert.Equal("$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", Htpasswd.Apr1("test", "H6uskkkW")); [Theory] [InlineData("test", "test")] // apr1 [InlineData("agent", "agent")] // bcrypt $2y$ [InlineData("legacy", "legacy")] // {SHA} public async Task TheMachineDoorCredentialsReadTheApi(string user, string password) { var client = As(user, password); Assert.Equal(HttpStatusCode.OK, (await client.GetAsync("/api/v1/tags")).StatusCode); Assert.Equal(HttpStatusCode.BadRequest, (await client.PostAsJsonAsync("/rank", new { query = " " })).StatusCode); } [Theory] [InlineData("test", "wrong")] [InlineData("nobody", "test")] [InlineData("agent", "")] [InlineData("ghost", "ghost")] // only in a hidden entry public async Task WrongCredentialsAre401(string user, string password) => Assert.Equal(HttpStatusCode.Unauthorized, (await As(user, password).GetAsync("/api/v1/tags")).StatusCode); [Fact] public async Task BasicCredentialsCannotWrite() { var r = await As("test", "test").PostAsJsonAsync("/api/v1/facts", new { title = "t", source = "s", kind = "fact", content = new string('x', 50) }); Assert.Equal(HttpStatusCode.Unauthorized, r.StatusCode); } [Fact] public async Task BasicCredentialsDoNotOpenTheUi() => Assert.Equal(HttpStatusCode.Redirect, (await As("test", "test").GetAsync("/facts")).StatusCode); }