removed webauthn stuff, changed to password proxy

This commit is contained in:
Rudis Muiznieks 2024-04-28 15:03:15 -05:00
parent 55f187c9aa
commit e21c69774a
Signed by: rudism
GPG Key ID: CABF2F86EF7884F9
7 changed files with 43 additions and 276 deletions

View File

@ -9,4 +9,4 @@ from mcr.microsoft.com/dotnet/aspnet:7.0
workdir /App workdir /App
copy --from=build-env /App/out . copy --from=build-env /App/out .
entrypoint ["dotnet", "webauthn-proxy.dll"] entrypoint ["dotnet", "auth-proxy.dll"]

View File

@ -1 +1 @@
mostly taken from https://github.com/ferdinandkeil/nginxwebauthn Single user password proxy.

View File

@ -13,7 +13,7 @@ indent_style = space
[*.{cs,csx,vb,vbx}] [*.{cs,csx,vb,vbx}]
indent_size = 2 indent_size = 2
insert_final_newline = true insert_final_newline = true
charset = utf-8-bom charset = utf-8
# XML project files # XML project files
[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}]
@ -44,6 +44,9 @@ dotnet_diagnostic.IDE0055.severity = warning
# CA1848: Use LoggerDelegates # CA1848: Use LoggerDelegates
dotnet_diagnostic.CA1848.severity = none dotnet_diagnostic.CA1848.severity = none
# CA1861: Prefer static readonly arrays
dotnet_diagnostic.CA1861.severity = none
# Sort using and Import directives with System.* appearing first # Sort using and Import directives with System.* appearing first
dotnet_sort_system_directives_first = true dotnet_sort_system_directives_first = true
dotnet_separate_import_directive_groups = false dotnet_separate_import_directive_groups = false

View File

@ -1,57 +1,31 @@
using System.Globalization; using System.Globalization;
using System.Net; using System.Net;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Fido2NetLib;
using Fido2NetLib.Objects;
using Microsoft.Data.Sqlite; using Microsoft.Data.Sqlite;
using SQLitePCL; using SQLitePCL;
namespace WebauthnProxy; namespace AuthProxy;
public class AddKeyRequest {
public required string Password { get; set; }
public required AuthenticatorAttestationRawResponse Response { get; set; }
}
public static class Program { public static class Program {
private const string COOKIE_NAME = "__Secure-Token"; 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( private static readonly string s_loginHtmlCache = File.ReadAllText(Path.Combine(
AppDomain.CurrentDomain.BaseDirectory, "login.html")); AppDomain.CurrentDomain.BaseDirectory, "login.html"));
private static readonly string? s_domain = Environment private static readonly string? s_domain = Environment
.GetEnvironmentVariable("WEBAUTHN_DOMAIN"); .GetEnvironmentVariable("AUTH_DOMAIN");
private static readonly string s_db = Environment 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 private static readonly long s_lifetime = long.Parse(Environment
.GetEnvironmentVariable("WEBAUTHN_LIFETIME") ?? "7", .GetEnvironmentVariable("AUTH_LIFETIME") ?? "7",
CultureInfo.InvariantCulture) * 60 * 60 * 24; CultureInfo.InvariantCulture) * 60 * 60 * 24;
private static readonly string? s_password = Environment private static readonly string? s_password = Environment
.GetEnvironmentVariable("WEBAUTHN_PASSWORD"); .GetEnvironmentVariable("AUTH_PASSWORD");
private static readonly int s_port = int.Parse(Environment private static readonly int s_port = int.Parse(Environment
.GetEnvironmentVariable("WEBAUTHN_PORT") ?? "5000", .GetEnvironmentVariable("AUTH_PORT") ?? "5000",
CultureInfo.InvariantCulture); CultureInfo.InvariantCulture);
private static readonly Dictionary<string, Fido2> s_fido2 = new();
private static readonly List<PublicKeyCredentialDescriptor> s_keys = new();
private static string ConnectionString { get => $"Data Source={s_db}"; } 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) { public static void Main(string[] args) {
var app = Initialize(args); var app = Initialize(args);
app.UseSession(); app.UseSession();
@ -101,130 +75,32 @@ public static class Program {
return Task.CompletedTask; return Task.CompletedTask;
}); });
app.MapPost("/auth/key", async (context) => { app.MapPost("/auth", async (context) => {
var options = GetFido2(context).GetAssertionOptions( if (context.Request.Form.TryGetValue("password", out var reqPassword)
s_keys, UserVerificationRequirement.Discouraged); && !string.IsNullOrEmpty(s_password)
context.Session.SetString(SESS_ASSERTION_KEY, && string.Equals(reqPassword, s_password, StringComparison.Ordinal)) {
JsonSerializer.Serialize(options)); var cookieOpts = new CookieOptions {
await context.Response.WriteAsJsonAsync(options); Path = "/",
}); Secure = true,
HttpOnly = true,
app.MapPost("/auth/complete", async (context) => { SameSite = SameSiteMode.None,
var assertionResponse = await context.Request MaxAge = TimeSpan.FromSeconds(s_lifetime),
.ReadFromJsonAsync<AuthenticatorAssertionRawResponse>(); };
if (assertionResponse == null) { if (!string.IsNullOrEmpty(s_domain)) {
context.Response.StatusCode = 401; cookieOpts.Domain = s_domain;
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;
} }
}
var res = await GetFido2(context).MakeAssertionAsync( using var connection = new SqliteConnection(ConnectionString);
assertionResponse, opts, pubKey, 0, (_, _) => Task.FromResult(true)); await connection.OpenAsync();
if (res.Status != "ok") { context.Response.Cookies.Append(
context.Response.StatusCode = 401; COOKIE_NAME,
await context.Response.WriteAsJsonAsync(new { error = "auth failed" }); GenerateToken(connection),
return; cookieOpts);
} await context.Response.WriteAsJsonAsync(new { status = "ok" });
} else {
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<PublicKeyCredentialDescriptor>(),
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<AddKeyRequest>();
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) {
context.Response.StatusCode = 401; context.Response.StatusCode = 401;
await context.Response.WriteAsJsonAsync(new { error = "bad password" }); 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}"); app.Run($"http://0.0.0.0:{s_port}");
@ -287,7 +163,7 @@ public static class Program {
connection.Open(); connection.Open();
using (var cmd = new SqliteCommand( using (var cmd = new SqliteCommand(
@"select 1 from sqlite_master @"select 1 from sqlite_master
where type='table' and name='credentials'", where type='table' and name='tokens'",
connection)) { connection)) {
var exists = cmd.ExecuteScalar(); var exists = cmd.ExecuteScalar();
if (exists == null) { if (exists == null) {
@ -304,19 +180,6 @@ public static class Program {
RemoveExpiredTokens(); 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); var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDistributedMemoryCache(); builder.Services.AddDistributedMemoryCache();
builder.Services.AddSession(); builder.Services.AddSession();

View File

@ -1,15 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Fido2" Version="3.0.1" /> <PackageReference Include="Microsoft.Data.SQLite" Version="8.0.4" />
<PackageReference Include="Fido2.Models" Version="3.0.1" />
<PackageReference Include="Microsoft.Data.SQLite" Version="7.0.5" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -1,102 +1,11 @@
<!doctype html> <!doctype html>
<html lang='en'> <html lang='en'>
<head>
<title>RDSM.ca Login</title>
<body> <body>
<script> <form action='/auth' method='post'>
function atobarray(sBase64) { <input type='password' name='password' />
const sBinaryString = atob(sBase64.replace(/-/g, '+').replace(/_/g, '/')); <input type='submit' value='Login' />
const aBinaryView = new Uint8Array(sBinaryString.length); </form>
Array.prototype.forEach.call(aBinaryView, function (el, idx, arr) {
arr[idx] = sBinaryString.charCodeAt(idx);
});
return aBinaryView;
}
function barraytoa(arrayBuffer) {
return btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)));
}
async function configure() {
try {
const data = await fetch('/auth/new-key', {
method: 'POST'
});
const json = await data.json();
json.challenge = atobarray(json.challenge);
json.user.id = atobarray(json.user.id);
const cred = await navigator.credentials.create({ publicKey: json });
var password = prompt('Administrative Password');
const addResp = await fetch('/auth/add-key', {
method: 'POST',
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
password,
response : {
id: cred.id,
rawId: barraytoa(cred.rawId),
type: cred.type,
response: {
clientDataJSON: barraytoa(cred.response.clientDataJSON),
attestationObject: barraytoa(cred.response.attestationObject),
},
},
}),
});
} catch (e) {
console.log(e)
}
}
(async function init() {
try {
const data = await fetch('/auth/key', {
method: 'POST'
});
const json = await data.json();
let result;
if (json.allowCredentials !== undefined && json.allowCredentials.length > 0) {
json.challenge = atobarray(json.challenge);
for (let i = 0; i < json.allowCredentials.length; i++) {
json.allowCredentials[i].id =
atobarray(json.allowCredentials[i].id);
}
try {
result = await navigator.credentials.get({ publicKey: json });
} catch(e) {
console.log(e);
await configure();
return;
}
await fetch('/auth/complete', {
method: 'POST',
body: JSON.stringify({
id: result.id,
rawId: barraytoa(result.rawId),
type: result.type,
response: {
authenticatorData: barraytoa(result.response.authenticatorData),
clientDataJSON: barraytoa(result.response.clientDataJSON),
signature: barraytoa(result.response.signature),
},
}),
headers: { 'Content-Type': 'application/json' }
});
const params = await new URLSearchParams(window.location.search);
if (params.has('target')) {
window.location.href = params.get('target');
} else {
window.location.href = '/';
}
} else {
await configure();
window.location.href='/auth/login';
}
} catch(e) {
console.log(e);
}
})()
</script>
<div id="command"></div>
</body> </body>
</html> </html>

View File

@ -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 ( create table tokens (
token text primary key, token text primary key,
created_date datetime not null default CURRENT_TIMESTAMP created_date datetime not null default CURRENT_TIMESTAMP