removed webauthn stuff, changed to password proxy
This commit is contained in:
parent
55f187c9aa
commit
e21c69774a
|
@ -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"]
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
mostly taken from https://github.com/ferdinandkeil/nginxwebauthn
|
Single user password proxy.
|
||||||
|
|
|
@ -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
|
||||||
|
|
195
src/Program.cs
195
src/Program.cs
|
@ -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();
|
||||||
|
|
|
@ -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>
|
103
src/login.html
103
src/login.html
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue