From 09f038e1e33c1775a3cceb10acd14cc22d102569 Mon Sep 17 00:00:00 2001 From: exyi Date: Thu, 22 Oct 2020 17:01:01 +0000 Subject: [PATCH] Better auth system for tasks.json Development more works as before. On Production, we store POST requests in tasks-{uid}.json that is only seved to the specific user. --- .../Controllers/TasksController.cs | 42 ++++-- server/Ksp.WebServer/KspAuthenticator.cs | 142 ++++++++++++++++++ server/Ksp.WebServer/Startup.cs | 1 + 3 files changed, 176 insertions(+), 9 deletions(-) create mode 100644 server/Ksp.WebServer/KspAuthenticator.cs diff --git a/server/Ksp.WebServer/Controllers/TasksController.cs b/server/Ksp.WebServer/Controllers/TasksController.cs index d254e70..bc9fee4 100644 --- a/server/Ksp.WebServer/Controllers/TasksController.cs +++ b/server/Ksp.WebServer/Controllers/TasksController.cs @@ -16,32 +16,56 @@ namespace Ksp.WebServer.Controllers { private readonly ILogger logger; private readonly IWebHostEnvironment env; + private readonly KspAuthenticator auth; - public TasksController(ILogger logger, IWebHostEnvironment env) + public TasksController(ILogger logger, IWebHostEnvironment env, KspAuthenticator auth) { + this.auth = auth; this.env = env; this.logger = logger; } - string TasksJsonFile => Path.Combine(env.ContentRootPath, "../../tasks.json"); + string TasksJsonFile(string suffix) => Path.Combine(env.ContentRootPath, $"../../tasks{suffix}.json"); + + string KspAuthCookie => this.HttpContext.Request.Cookies["ksp_auth"]; [HttpGet] public IActionResult Get() { - return this.PhysicalFile(TasksJsonFile, "text/json"); + string file = null; + if (KspAuthCookie is object) + { + var user = KspAuthenticator.ParseAuthCookie(KspAuthCookie); + file = TasksJsonFile("-" + user.Id.Value); + if (!System.IO.File.Exists(file)) + file = null; + } + + file ??= TasksJsonFile(""); + + return this.PhysicalFile(TasksJsonFile(""), "text/json"); } [HttpPost] public async Task Post() { - // Saving in production is now enabled in order to allow people from the outside - // to modify tasks.json using the editor. - // if (env.IsProduction()) - // return this.Forbid(); + string suffix; + if (env.IsDevelopment()) + { + suffix = ""; + } + else + { + if (KspAuthCookie is null) + return StatusCode(401); + var user = await auth.VerifyUser(KspAuthCookie); + if (user == null) + return StatusCode(403); + suffix = "-" + user.Id.Value; + } - // TODO: auth org using var rdr = new StreamReader(HttpContext.Request.Body); - await System.IO.File.WriteAllTextAsync(TasksJsonFile, await rdr.ReadToEndAsync()); + await System.IO.File.WriteAllTextAsync(TasksJsonFile(suffix), await rdr.ReadToEndAsync()); return Ok(); } } diff --git a/server/Ksp.WebServer/KspAuthenticator.cs b/server/Ksp.WebServer/KspAuthenticator.cs new file mode 100644 index 0000000..a3583ce --- /dev/null +++ b/server/Ksp.WebServer/KspAuthenticator.cs @@ -0,0 +1,142 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using AngleSharp.Html.Dom; +using AngleSharp.Html.Parser; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Ksp.WebServer +{ + public class KspAuthenticator + { + readonly KspProxyConfig kspProxyConfig; + readonly ILogger logger; + + // request to https://ksp.mff.cuni.cz/auth/manage.cgi?mode=change + // contains user information + + public KspAuthenticator( + IOptions kspProxyConfig, + ILogger logger) + { + this.kspProxyConfig = kspProxyConfig.Value; + this.logger = logger; + } + + public static UnverifiedAuthCookie ParseAuthCookie(string cookie) + { + var s = cookie.Split(':'); + return new UnverifiedAuthCookie( + UserId.Parse(s[1]), + s[3], + s[2].Split(',') + ); + } + + async Task FetchPage(string url, string authCookie) + { + Console.WriteLine($"AuthCookie={authCookie}"); + var cookies = new CookieContainer(); + cookies.Add(new Uri(kspProxyConfig.Host), new Cookie("ksp_auth", Uri.EscapeDataString(authCookie))); + var c = new HttpClient(new HttpClientHandler { CookieContainer = cookies }); + var rq = new HttpRequestMessage(HttpMethod.Get, $"{kspProxyConfig.Host}/{url}"); + rq.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/html")); + rq.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xhtml+xml")); + if (!string.IsNullOrEmpty(kspProxyConfig.Authorization)) + rq.Headers.Authorization = + AuthenticationHeaderValue.Parse(kspProxyConfig.Authorization); + var rs = await c.SendAsync(rq); + logger.LogInformation("Verification request for response {code}", rs.StatusCode); + if (rs.StatusCode != HttpStatusCode.OK) + return null; + var response = await rs.Content.ReadAsStringAsync(); + var parser = new HtmlParser(); + return parser.ParseDocument(response); + } + + public async Task VerifyUser(string cookie) + { + var parsedCookie = ParseAuthCookie(cookie); + logger.LogInformation("Verifying user {uid}", parsedCookie.Id); + var page = await FetchPage("auth/manage.cgi?mode=change", cookie); + var form = page?.QuerySelector("#content form"); + var email = (form?.QuerySelector("input#email") as IHtmlInputElement)?.Value; + var userName = (form?.QuerySelector("input#logname") as IHtmlInputElement)?.Value; + var showName = (form?.QuerySelector("input#showname") as IHtmlInputElement)?.Value; + if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(userName) || string.IsNullOrEmpty(showName)) + { + return null; + } + return new VerifiedUserInfo( + parsedCookie.Id, + showName, + parsedCookie.Roles, + email, + userName + ); + } + } + + public sealed class UserId + { + public int Value { get; } + public UserId(int value) + { + this.Value = value; + } + + public override bool Equals(object obj) + { + return obj is UserId id && + Value == id.Value; + } + + public override int GetHashCode() + { + return HashCode.Combine(Value); + } + + public override string ToString() => $"uid[{Value}]"; + public static UserId Parse(string val) + { + var id = int.Parse(val); + if (id <= 0) throw new Exception(); + return new UserId(id); + } + } + + // Cookie, for example 1603380331:1662:auth_master,org:Standa=20Luke=c5=a1:SIGNATURE + public sealed class UnverifiedAuthCookie + { + public UserId Id { get; } + public string FullName { get; } + public string[] Roles { get; } + public UnverifiedAuthCookie(UserId id, string fullName, string[] roles) + { + this.Id = id; + this.FullName = fullName; + this.Roles = roles; + } + } + + public sealed class VerifiedUserInfo + { + public UserId Id { get; } + public string FullName { get; } + public string[] Roles { get; } + public string Email { get; } + public string UserName { get; } + + public VerifiedUserInfo(UserId id, string fullName, string[] roles, string email, string userName) + { + this.Id = id; + this.FullName = fullName; + this.Roles = roles; + this.Email = email; + this.UserName = userName; + } + } +} diff --git a/server/Ksp.WebServer/Startup.cs b/server/Ksp.WebServer/Startup.cs index 4c3946b..cae725c 100644 --- a/server/Ksp.WebServer/Startup.cs +++ b/server/Ksp.WebServer/Startup.cs @@ -47,6 +47,7 @@ namespace Ksp.WebServer services.AddProxies(); services.Configure(Configuration.GetSection(nameof(KspProxyConfig))); services.AddSingleton(); + services.AddSingleton(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.