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.
This commit is contained in:
parent
ce9760a84b
commit
09f038e1e3
3 changed files with 176 additions and 9 deletions
|
@ -16,32 +16,56 @@ namespace Ksp.WebServer.Controllers
|
||||||
{
|
{
|
||||||
private readonly ILogger<TasksController> logger;
|
private readonly ILogger<TasksController> logger;
|
||||||
private readonly IWebHostEnvironment env;
|
private readonly IWebHostEnvironment env;
|
||||||
|
private readonly KspAuthenticator auth;
|
||||||
|
|
||||||
public TasksController(ILogger<TasksController> logger, IWebHostEnvironment env)
|
public TasksController(ILogger<TasksController> logger, IWebHostEnvironment env, KspAuthenticator auth)
|
||||||
{
|
{
|
||||||
|
this.auth = auth;
|
||||||
this.env = env;
|
this.env = env;
|
||||||
this.logger = logger;
|
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]
|
[HttpGet]
|
||||||
public IActionResult Get()
|
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]
|
[HttpPost]
|
||||||
public async Task<IActionResult> Post()
|
public async Task<IActionResult> Post()
|
||||||
{
|
{
|
||||||
// Saving in production is now enabled in order to allow people from the outside
|
string suffix;
|
||||||
// to modify tasks.json using the editor.
|
if (env.IsDevelopment())
|
||||||
// if (env.IsProduction())
|
{
|
||||||
// return this.Forbid();
|
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);
|
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();
|
return Ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
142
server/Ksp.WebServer/KspAuthenticator.cs
Normal file
142
server/Ksp.WebServer/KspAuthenticator.cs
Normal file
|
@ -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<KspAuthenticator> logger;
|
||||||
|
|
||||||
|
// request to https://ksp.mff.cuni.cz/auth/manage.cgi?mode=change
|
||||||
|
// contains user information
|
||||||
|
|
||||||
|
public KspAuthenticator(
|
||||||
|
IOptions<KspProxyConfig> kspProxyConfig,
|
||||||
|
ILogger<KspAuthenticator> 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<IHtmlDocument> 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<VerifiedUserInfo> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -47,6 +47,7 @@ namespace Ksp.WebServer
|
||||||
services.AddProxies();
|
services.AddProxies();
|
||||||
services.Configure<KspProxyConfig>(Configuration.GetSection(nameof(KspProxyConfig)));
|
services.Configure<KspProxyConfig>(Configuration.GetSection(nameof(KspProxyConfig)));
|
||||||
services.AddSingleton<KspPageRewriter>();
|
services.AddSingleton<KspPageRewriter>();
|
||||||
|
services.AddSingleton<KspAuthenticator>();
|
||||||
}
|
}
|
||||||
|
|
||||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||||
|
|
Reference in a new issue