diff --git a/Dockerfile b/Dockerfile index bbfac09..11abd3f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,4 +9,4 @@ from mcr.microsoft.com/dotnet/aspnet:7.0 workdir /App copy --from=build-env /App/out . -entrypoint ["dotnet", "webauthn-proxy.dll"] +entrypoint ["dotnet", "auth-proxy.dll"] diff --git a/README.md b/README.md index bb66f25..4e7cc37 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -mostly taken from https://github.com/ferdinandkeil/nginxwebauthn +Single user password proxy. diff --git a/src/.editorconfig b/src/.editorconfig index b69fd05..73378d9 100644 --- a/src/.editorconfig +++ b/src/.editorconfig @@ -13,7 +13,7 @@ indent_style = space [*.{cs,csx,vb,vbx}] indent_size = 2 insert_final_newline = true -charset = utf-8-bom +charset = utf-8 # XML project files [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] @@ -44,6 +44,9 @@ dotnet_diagnostic.IDE0055.severity = warning # CA1848: Use LoggerDelegates dotnet_diagnostic.CA1848.severity = none +# CA1861: Prefer static readonly arrays +dotnet_diagnostic.CA1861.severity = none + # Sort using and Import directives with System.* appearing first dotnet_sort_system_directives_first = true dotnet_separate_import_directive_groups = false diff --git a/src/Program.cs b/src/Program.cs index c6478cc..0f3a840 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -1,57 +1,31 @@ -using System.Globalization; +using System.Globalization; using System.Net; using System.Security.Cryptography; -using System.Text; -using System.Text.Json; -using Fido2NetLib; -using Fido2NetLib.Objects; using Microsoft.Data.Sqlite; using SQLitePCL; -namespace WebauthnProxy; - -public class AddKeyRequest { - public required string Password { get; set; } - public required AuthenticatorAttestationRawResponse Response { get; set; } -} +namespace AuthProxy; public static class Program { private const string COOKIE_NAME = "__Secure-Token"; - private const string SESS_ATTESTATION_KEY = "fido2.attestationOptions"; - private const string SESS_ASSERTION_KEY = "fido2.assertionOptions"; private static readonly string s_loginHtmlCache = File.ReadAllText(Path.Combine( AppDomain.CurrentDomain.BaseDirectory, "login.html")); private static readonly string? s_domain = Environment - .GetEnvironmentVariable("WEBAUTHN_DOMAIN"); + .GetEnvironmentVariable("AUTH_DOMAIN"); private static readonly string s_db = Environment - .GetEnvironmentVariable("WEBAUTHN_DB") ?? "credentials.db"; + .GetEnvironmentVariable("AUTH_DB") ?? "credentials.db"; private static readonly long s_lifetime = long.Parse(Environment - .GetEnvironmentVariable("WEBAUTHN_LIFETIME") ?? "7", + .GetEnvironmentVariable("AUTH_LIFETIME") ?? "7", CultureInfo.InvariantCulture) * 60 * 60 * 24; private static readonly string? s_password = Environment - .GetEnvironmentVariable("WEBAUTHN_PASSWORD"); + .GetEnvironmentVariable("AUTH_PASSWORD"); private static readonly int s_port = int.Parse(Environment - .GetEnvironmentVariable("WEBAUTHN_PORT") ?? "5000", + .GetEnvironmentVariable("AUTH_PORT") ?? "5000", CultureInfo.InvariantCulture); - private static readonly Dictionary s_fido2 = new(); - private static readonly List s_keys = new(); private static string ConnectionString { get => $"Data Source={s_db}"; } - private static Fido2 GetFido2(HttpContext context) { - var origin = context.Request.Headers["Origin"].First()!; - if (!s_fido2.ContainsKey(origin)) { - s_fido2.Add(origin, new(new Fido2Configuration { - ServerDomain = s_domain ?? context.Request.Host.Value.Split(":").First(), - ServerName = "WebauthnProxy", - Origins = new(new[] { origin }), - })); - } - - return s_fido2[origin]; - } - public static void Main(string[] args) { var app = Initialize(args); app.UseSession(); @@ -101,130 +75,32 @@ public static class Program { return Task.CompletedTask; }); - app.MapPost("/auth/key", async (context) => { - var options = GetFido2(context).GetAssertionOptions( - s_keys, UserVerificationRequirement.Discouraged); - context.Session.SetString(SESS_ASSERTION_KEY, - JsonSerializer.Serialize(options)); - await context.Response.WriteAsJsonAsync(options); - }); - - app.MapPost("/auth/complete", async (context) => { - var assertionResponse = await context.Request - .ReadFromJsonAsync(); - if (assertionResponse == null) { - context.Response.StatusCode = 401; - await context.Response.WriteAsJsonAsync(new { error = "bad response" }); - return; - } - - var optsJson = context.Session.GetString(SESS_ASSERTION_KEY); - context.Session.Remove(SESS_ASSERTION_KEY); - var opts = AssertionOptions.FromJson(optsJson); - using var connection = new SqliteConnection(ConnectionString); - connection.Open(); - byte[]? pubKey; - using (var cmd = new SqliteCommand( - "select key from credentials where id=@Id", - connection)) { - var param = cmd.CreateParameter(); - param.ParameterName = "@Id"; - param.Value = Convert.ToBase64String(assertionResponse.Id); - cmd.Parameters.Add(param); - pubKey = cmd.ExecuteScalar() is string keyStr - ? Convert.FromBase64String(keyStr) - : null; - if (pubKey == null) { - context.Response.StatusCode = 401; - await context.Response.WriteAsJsonAsync(new { error = "bad key" }); - return; + app.MapPost("/auth", async (context) => { + if (context.Request.Form.TryGetValue("password", out var reqPassword) + && !string.IsNullOrEmpty(s_password) + && string.Equals(reqPassword, 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; } - } - var res = await GetFido2(context).MakeAssertionAsync( - assertionResponse, opts, pubKey, 0, (_, _) => Task.FromResult(true)); - if (res.Status != "ok") { - context.Response.StatusCode = 401; - await context.Response.WriteAsJsonAsync(new { error = "auth failed" }); - return; - } - - 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, - GenerateToken(connection), - cookieOpts); - await context.Response.WriteAsJsonAsync(new { status = "ok" }); - }); - - app.MapPost("/auth/new-key", async (context) => { - var user = new Fido2User { - Id = Encoding.UTF8.GetBytes("default"), - Name = "Default User", - DisplayName = "Default User", - }; - var options = GetFido2(context).RequestNewCredential( - user, - new List(), - AuthenticatorSelection.Default, - AttestationConveyancePreference.None); - context.Session.SetString(SESS_ATTESTATION_KEY, - JsonSerializer.Serialize(options)); - - await context.Response.WriteAsJsonAsync(options); - }); - - app.MapPost("/auth/add-key", async (context) => { - var req = await context.Request.ReadFromJsonAsync(); - if (req == null) { - context.Response.StatusCode = 400; - await context.Response.WriteAsJsonAsync(new { error = "bad request" }); - return; - } - - if (string.IsNullOrEmpty(s_password) - || s_password != req.Password) { + using var connection = new SqliteConnection(ConnectionString); + await connection.OpenAsync(); + context.Response.Cookies.Append( + COOKIE_NAME, + GenerateToken(connection), + cookieOpts); + await context.Response.WriteAsJsonAsync(new { status = "ok" }); + } else { context.Response.StatusCode = 401; await context.Response.WriteAsJsonAsync(new { error = "bad password" }); - return; } - - var optsJson = context.Session.GetString(SESS_ATTESTATION_KEY); - context.Session.Remove(SESS_ATTESTATION_KEY); - var opts = CredentialCreateOptions.FromJson(optsJson); - var cred = await GetFido2(context).MakeNewCredentialAsync( - req.Response, opts, (_, _) => Task.FromResult(true)); - var descriptor = new PublicKeyCredentialDescriptor( - cred.Result!.CredentialId); - - // store in list and save to database - s_keys.Add(descriptor); - using var connection = new SqliteConnection(ConnectionString); - connection.Open(); - using var cmd = new SqliteCommand( - @"insert into credentials (id, key) values (@Id, @Key)", - connection); - var idParam = cmd.CreateParameter(); - idParam.ParameterName = "@Id"; - idParam.Value = Convert.ToBase64String(cred.Result.CredentialId); - var keyParam = cmd.CreateParameter(); - keyParam.ParameterName = "@Key"; - keyParam.Value = Convert.ToBase64String(cred.Result.PublicKey); - cmd.Parameters.Add(idParam); - cmd.Parameters.Add(keyParam); - await cmd.ExecuteNonQueryAsync(); - - await context.Response.WriteAsJsonAsync(new { status = "ok" }); }); app.Run($"http://0.0.0.0:{s_port}"); @@ -287,7 +163,7 @@ public static class Program { connection.Open(); using (var cmd = new SqliteCommand( @"select 1 from sqlite_master - where type='table' and name='credentials'", + where type='table' and name='tokens'", connection)) { var exists = cmd.ExecuteScalar(); if (exists == null) { @@ -304,19 +180,6 @@ public static class Program { RemoveExpiredTokens(); - // read credentials - using (var cmd = new SqliteCommand( - @"select id from credentials", - connection)) { - var reader = cmd.ExecuteReader(); - while (reader.Read()) { - var id = reader.GetString(0); - var desc = new PublicKeyCredentialDescriptor( - Convert.FromBase64String(id)); - if (desc != null) s_keys.Add(desc); - } - } - var builder = WebApplication.CreateBuilder(args); builder.Services.AddDistributedMemoryCache(); builder.Services.AddSession(); diff --git a/src/webauthn-proxy.csproj b/src/auth-proxy.csproj similarity index 65% rename from src/webauthn-proxy.csproj rename to src/auth-proxy.csproj index 1812153..501b1b2 100644 --- a/src/webauthn-proxy.csproj +++ b/src/auth-proxy.csproj @@ -1,15 +1,13 @@ - net7.0 + net8.0 enable enable - - - + diff --git a/src/login.html b/src/login.html index 7c1f472..9d7d511 100644 --- a/src/login.html +++ b/src/login.html @@ -1,102 +1,11 @@ + + RDSM.ca Login - -
+
+ + +
diff --git a/src/schema.sql b/src/schema.sql index 51be780..fa380eb 100644 --- a/src/schema.sql +++ b/src/schema.sql @@ -1,9 +1,3 @@ -create table credentials ( - id text primary key, - key text not null, - created_date datetime not null default CURRENT_TIMESTAMP -); - create table tokens ( token text primary key, created_date datetime not null default CURRENT_TIMESTAMP