removed webauthn stuff, changed to password proxy
This commit is contained in:
parent
55f187c9aa
commit
e21c69774a
7 changed files with 43 additions and 276 deletions
|
@ -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"]
|
||||
|
|
|
@ -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}]
|
||||
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
|
||||
|
|
195
src/Program.cs
195
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<string, Fido2> s_fido2 = new();
|
||||
private static readonly List<PublicKeyCredentialDescriptor> 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<AuthenticatorAssertionRawResponse>();
|
||||
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<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) {
|
||||
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();
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Fido2" Version="3.0.1" />
|
||||
<PackageReference Include="Fido2.Models" Version="3.0.1" />
|
||||
<PackageReference Include="Microsoft.Data.SQLite" Version="7.0.5" />
|
||||
<PackageReference Include="Microsoft.Data.SQLite" Version="8.0.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
103
src/login.html
103
src/login.html
|
@ -1,102 +1,11 @@
|
|||
<!doctype html>
|
||||
<html lang='en'>
|
||||
<head>
|
||||
<title>RDSM.ca Login</title>
|
||||
<body>
|
||||
<script>
|
||||
function atobarray(sBase64) {
|
||||
const sBinaryString = atob(sBase64.replace(/-/g, '+').replace(/_/g, '/'));
|
||||
const aBinaryView = new Uint8Array(sBinaryString.length);
|
||||
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>
|
||||
<form action='/auth' method='post'>
|
||||
<input type='password' name='password' />
|
||||
<input type='submit' value='Login' />
|
||||
</form>
|
||||
</body>
|
||||
</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 (
|
||||
token text primary key,
|
||||
created_date datetime not null default CURRENT_TIMESTAMP
|
||||
|
|
Loading…
Reference in a new issue