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 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.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<IActionResult> 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();
|
||||
}
|
||||
}
|
||||
|
|
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.Configure<KspProxyConfig>(Configuration.GetSection(nameof(KspProxyConfig)));
|
||||
services.AddSingleton<KspPageRewriter>();
|
||||
services.AddSingleton<KspAuthenticator>();
|
||||
}
|
||||
|
||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||
|
|
Reference in a new issue