190 lines
6.4 KiB
C#
190 lines
6.4 KiB
C#
using System.Globalization;
|
|
using System.Net;
|
|
using System.Security.Cryptography;
|
|
using Microsoft.Data.Sqlite;
|
|
using SQLitePCL;
|
|
|
|
namespace AuthProxy;
|
|
|
|
public static class Program {
|
|
private const string COOKIE_NAME = "__Secure-Token";
|
|
|
|
private static readonly string s_loginHtmlCache = File.ReadAllText(Path.Combine(
|
|
AppDomain.CurrentDomain.BaseDirectory, "login.html"));
|
|
private static readonly string? s_domain = Environment
|
|
.GetEnvironmentVariable("AUTH_DOMAIN");
|
|
private static readonly string s_db = Environment
|
|
.GetEnvironmentVariable("AUTH_DB") ?? "credentials.db";
|
|
private static readonly long s_lifetime = long.Parse(Environment
|
|
.GetEnvironmentVariable("AUTH_LIFETIME") ?? "7",
|
|
CultureInfo.InvariantCulture) * 60 * 60 * 24;
|
|
private static readonly string? s_password = Environment
|
|
.GetEnvironmentVariable("AUTH_PASSWORD");
|
|
private static readonly int s_port = int.Parse(Environment
|
|
.GetEnvironmentVariable("AUTH_PORT") ?? "5000",
|
|
CultureInfo.InvariantCulture);
|
|
|
|
private static string ConnectionString { get => $"Data Source={s_db}"; }
|
|
|
|
public static void Main(string[] args) {
|
|
var app = Initialize(args);
|
|
app.UseSession();
|
|
|
|
app.MapGet("/auth/check", async (context) => {
|
|
var token = context.Request.Cookies[COOKIE_NAME];
|
|
if (!TokenIsValid(token)) {
|
|
context.Response.ContentType = "text/plain";
|
|
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
|
|
await context.Response.WriteAsync("unauthorized");
|
|
return;
|
|
}
|
|
|
|
context.Response.ContentType = "text/plain";
|
|
context.Response.StatusCode = (int)HttpStatusCode.OK;
|
|
await context.Response.WriteAsync("authorized");
|
|
});
|
|
|
|
app.MapGet("/auth/login", async (context) => {
|
|
context.Response.ContentType = "text/html";
|
|
context.Response.StatusCode = (int)HttpStatusCode.OK;
|
|
await context.Response.WriteAsync(s_loginHtmlCache);
|
|
});
|
|
|
|
app.MapGet("/auth/logout", (context) => {
|
|
if (context.Request.Cookies[COOKIE_NAME] == null)
|
|
return Task.CompletedTask;
|
|
|
|
var cookieOpts = new CookieOptions {
|
|
Path = "/",
|
|
Secure = true,
|
|
HttpOnly = true,
|
|
SameSite = SameSiteMode.None,
|
|
MaxAge = TimeSpan.FromSeconds(s_lifetime),
|
|
};
|
|
if (!string.IsNullOrEmpty(s_domain)) {
|
|
cookieOpts.Domain = s_domain;
|
|
}
|
|
|
|
context.Response.Cookies.Append(COOKIE_NAME, string.Empty, cookieOpts);
|
|
|
|
context.Response.Redirect("/");
|
|
return Task.CompletedTask;
|
|
});
|
|
|
|
app.MapPost("/auth/password", async (context) => {
|
|
if (context.Request.Form.TryGetValue("password", out var reqPassword)
|
|
&& !string.IsNullOrEmpty(s_password)
|
|
&& string.Equals(reqPassword.FirstOrDefault(), s_password, StringComparison.Ordinal)) {
|
|
var cookieOpts = new CookieOptions {
|
|
Path = "/",
|
|
Secure = true,
|
|
HttpOnly = true,
|
|
SameSite = SameSiteMode.None,
|
|
MaxAge = TimeSpan.FromSeconds(s_lifetime),
|
|
};
|
|
if (!string.IsNullOrEmpty(s_domain)) {
|
|
cookieOpts.Domain = s_domain;
|
|
}
|
|
|
|
using var connection = new SqliteConnection(ConnectionString);
|
|
await connection.OpenAsync();
|
|
context.Response.Cookies.Append(
|
|
COOKIE_NAME,
|
|
GenerateToken(connection),
|
|
cookieOpts);
|
|
if (!context.Request.Form.TryGetValue("target", out var target)
|
|
|| string.IsNullOrEmpty(target.FirstOrDefault())) {
|
|
target = [];
|
|
}
|
|
|
|
context.Response.Redirect(target.FirstOrDefault() ?? "/");
|
|
} else {
|
|
context.Response.StatusCode = 401;
|
|
await context.Response.WriteAsJsonAsync(new { error = "bad password" });
|
|
}
|
|
});
|
|
|
|
app.Run($"http://0.0.0.0:{s_port}");
|
|
}
|
|
|
|
private static string GenerateToken(SqliteConnection connection) {
|
|
var rng = RandomNumberGenerator.Create();
|
|
var bytes = new byte[32];
|
|
rng.GetBytes(bytes);
|
|
var token = Convert.ToBase64String(bytes);
|
|
using var cmd = new SqliteCommand(@"insert into tokens (token) values (@Token)",
|
|
connection);
|
|
var param = cmd.CreateParameter();
|
|
param.ParameterName = "@Token";
|
|
param.Value = token;
|
|
cmd.Parameters.Add(param);
|
|
cmd.ExecuteNonQuery();
|
|
return token;
|
|
}
|
|
|
|
private static bool TokenIsValid(string? token) {
|
|
if (string.IsNullOrWhiteSpace(token)) return false;
|
|
|
|
using var connection = new SqliteConnection(ConnectionString);
|
|
connection.Open();
|
|
using var cmd = new SqliteCommand(
|
|
@"select 1 from tokens where
|
|
token=@Token
|
|
and julianday(created_date, @Timeout) > julianday(CURRENT_TIMESTAMP)",
|
|
connection);
|
|
var tokenParam = cmd.CreateParameter();
|
|
tokenParam.ParameterName = "@Token";
|
|
tokenParam.Value = token;
|
|
var timeoutParam = cmd.CreateParameter();
|
|
timeoutParam.ParameterName = "@Timeout";
|
|
timeoutParam.Value = $"+{s_lifetime} seconds";
|
|
cmd.Parameters.Add(tokenParam);
|
|
cmd.Parameters.Add(timeoutParam);
|
|
return cmd.ExecuteScalar() != null;
|
|
}
|
|
|
|
private static void RemoveExpiredTokens() {
|
|
using var connection = new SqliteConnection(ConnectionString);
|
|
connection.Open();
|
|
using var cmd = new SqliteCommand(
|
|
@"delete from tokens where
|
|
julianday(created_date, @Timeout) < julianday(CURRENT_TIMESTAMP)",
|
|
connection);
|
|
var timeoutParam = cmd.CreateParameter();
|
|
timeoutParam.ParameterName = "@Timeout";
|
|
timeoutParam.Value = $"+{s_lifetime} seconds";
|
|
cmd.Parameters.Add(timeoutParam);
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
|
|
private static WebApplication Initialize(string[] args) {
|
|
Batteries.Init();
|
|
|
|
using var connection = new SqliteConnection(ConnectionString);
|
|
connection.Open();
|
|
using (var cmd = new SqliteCommand(
|
|
@"select 1 from sqlite_master
|
|
where type='table' and name='tokens'",
|
|
connection)) {
|
|
var exists = cmd.ExecuteScalar();
|
|
if (exists == null) {
|
|
var schemaPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "schema.sql");
|
|
if (!File.Exists(schemaPath)) {
|
|
throw new FileNotFoundException(schemaPath);
|
|
}
|
|
|
|
var schema = File.ReadAllText(schemaPath);
|
|
using var schemaCmd = new SqliteCommand(schema, connection);
|
|
schemaCmd.ExecuteNonQuery();
|
|
}
|
|
}
|
|
|
|
RemoveExpiredTokens();
|
|
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
builder.Services.AddDistributedMemoryCache();
|
|
builder.Services.AddSession();
|
|
return builder.Build();
|
|
}
|
|
}
|