Browse Source

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.
mj-deploy
Standa Lukeš 4 years ago
parent
commit
09f038e1e3
  1. 42
      server/Ksp.WebServer/Controllers/TasksController.cs
  2. 142
      server/Ksp.WebServer/KspAuthenticator.cs
  3. 1
      server/Ksp.WebServer/Startup.cs

42
server/Ksp.WebServer/Controllers/TasksController.cs

@ -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

@ -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;
}
}
}

1
server/Ksp.WebServer/Startup.cs

@ -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.