c# rewrite
This commit is contained in:
parent
712e3294ac
commit
82bc13de67
11 changed files with 793 additions and 313 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
bin/
|
||||
obj/
|
||||
credentials.db
|
23
Dockerfile
23
Dockerfile
|
@ -1,11 +1,12 @@
|
|||
from alpine:latest
|
||||
run apk update && apk upgrade
|
||||
run apk add python3 py3-virtualenv
|
||||
run adduser -D -u 1000 webauthn
|
||||
copy . /opt/app
|
||||
run chown -R webauthn:webauthn /opt/app
|
||||
user webauthn
|
||||
workdir /opt/app
|
||||
run virtualenv venv
|
||||
run . venv/bin/activate && pip install -r requirements.txt
|
||||
cmd ["./run.sh"]
|
||||
from mcr.microsoft.com/dotnet/sdk:7.0 as build-env
|
||||
|
||||
workdir /App
|
||||
copy ./src/ ./
|
||||
run dotnet restore
|
||||
run dotnet publish -c Release -o out
|
||||
|
||||
from mcr.microsoft.com/dotnet/aspnet:7.0
|
||||
|
||||
workdir /App
|
||||
copy --from=build-env /App/out .
|
||||
entrypoint ["dotnet", "webauthn-proxy.dll"]
|
||||
|
|
296
main.py
296
main.py
|
@ -1,296 +0,0 @@
|
|||
from json.decoder import JSONDecodeError
|
||||
import datetime
|
||||
import random
|
||||
import socketserver
|
||||
import http.server
|
||||
import http.cookies
|
||||
import json
|
||||
import base64
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
|
||||
from fido2.client import ClientData
|
||||
from fido2.webauthn import PublicKeyCredentialRpEntity
|
||||
from fido2.server import U2FFido2Server
|
||||
from fido2.ctap2 import AttestationObject, AttestedCredentialData, AuthenticatorData
|
||||
from fido2 import cbor
|
||||
|
||||
TOKEN_LIFETIME = 60 * 60 * 24 * 7
|
||||
PORT = 8000
|
||||
FORM = """
|
||||
<body>
|
||||
<script>
|
||||
|
||||
function atobarray(sBase64) {
|
||||
var sBinaryString = atob(sBase64), 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 {
|
||||
let data = await fetch('/auth/get_challenge_for_new_key', { method: 'POST' });
|
||||
let json = await data.json()
|
||||
json.publicKey.challenge = atobarray(json.publicKey.challenge)
|
||||
json.publicKey.user.id = atobarray(json.publicKey.user.id)
|
||||
let cred = await navigator.credentials.create(json)
|
||||
window.command.innerHTML = 'On your server, to save this key please run:<br /><pre>python main.py save-client ' + window.location.host + ' ' + barraytoa(cred.response.clientDataJSON) + ' ' + barraytoa(cred.response.attestationObject) + '</pre>'
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
(async function init() {
|
||||
try {
|
||||
let data = await fetch('/auth/get_challenge_for_existing_key', { method: 'POST' });
|
||||
let json = await data.json()
|
||||
if (json.publicKey !== undefined) {
|
||||
json.publicKey.challenge = atobarray(json.publicKey.challenge)
|
||||
for (let i = 0; i < json.publicKey.allowCredentials.length; i++) {
|
||||
json.publicKey.allowCredentials[i].id = atobarray(json.publicKey.allowCredentials[i].id)
|
||||
}
|
||||
try {
|
||||
var result = await navigator.credentials.get(json)
|
||||
} catch(e) {
|
||||
console.log('unknown key')
|
||||
await configure()
|
||||
return
|
||||
}
|
||||
await fetch('/auth/complete_challenge_for_existing_key', { method: 'POST', body: JSON.stringify({
|
||||
id: barraytoa(result.rawId),
|
||||
authenticatorData: barraytoa(result.response.authenticatorData),
|
||||
clientDataJSON: barraytoa(result.response.clientDataJSON),
|
||||
signature: barraytoa(result.response.signature)
|
||||
}), headers:{ 'Content-Type': 'application/json' }})
|
||||
let params = await new URLSearchParams(window.location.search)
|
||||
if (params.has('target')) {
|
||||
window.location.href = params.get('target')
|
||||
} else {
|
||||
window.location.href = "/"
|
||||
}
|
||||
}
|
||||
if (json.error == 'not_configured') {
|
||||
await configure()
|
||||
}
|
||||
} catch(e) {
|
||||
console.log(e)
|
||||
}
|
||||
})()
|
||||
</script>
|
||||
<div id="command"></div>
|
||||
</body>
|
||||
"""
|
||||
|
||||
class TokenManager(object):
|
||||
"""Who needs a database when you can just store everything in memory?"""
|
||||
|
||||
def __init__(self):
|
||||
self.tokens = {}
|
||||
self.random = random.SystemRandom()
|
||||
|
||||
def generate(self):
|
||||
t = '%064x' % self.random.getrandbits(8*32)
|
||||
self.tokens[t] = time.time()
|
||||
return t
|
||||
|
||||
def is_valid(self, t):
|
||||
try:
|
||||
return time.time() - self.tokens.get(t, 0) < TOKEN_LIFETIME
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def invalidate(self, t):
|
||||
if t in self.tokens:
|
||||
del self.tokens[t]
|
||||
|
||||
CHALLENGE = {}
|
||||
TOKEN_MANAGER = TokenManager()
|
||||
|
||||
class AuthHandler(http.server.BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
if self.path == '/auth/check':
|
||||
cookie = http.cookies.SimpleCookie(self.headers.get('Cookie'))
|
||||
if '__Secure-Token' in cookie and TOKEN_MANAGER.is_valid(cookie['__Secure-Token'].value):
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
return
|
||||
|
||||
self.send_response(401)
|
||||
self.end_headers()
|
||||
return
|
||||
|
||||
if self.path[:11] == "/auth/login":
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'text/html')
|
||||
self.end_headers()
|
||||
self.wfile.write(bytes(FORM, 'UTF-8'))
|
||||
return
|
||||
|
||||
if self.path == '/auth/logout':
|
||||
cookie = http.cookies.SimpleCookie(self.headers.get('Cookie'))
|
||||
if '__Secure-Token' in cookie:
|
||||
TOKEN_MANAGER.invalidate(cookie['__Secure-Token'].value)
|
||||
|
||||
# This just replaces the token with garbage
|
||||
self.send_response(302)
|
||||
cookie = http.cookies.SimpleCookie()
|
||||
cookie["__Secure-Token"] = ''
|
||||
cookie["__Secure-Token"]["path"] = '/'
|
||||
cookie["__Secure-Token"]["secure"] = True
|
||||
cookie["__Secure-Token"]["max-age"] = 0 # remove the cookie ASAP
|
||||
self.send_header('Set-Cookie', cookie.output(header=''))
|
||||
self.send_header('Location', '/')
|
||||
self.end_headers()
|
||||
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
|
||||
def do_POST(self):
|
||||
origin = self.headers.get('Origin')
|
||||
host = origin[len('https://'):]
|
||||
|
||||
rp = PublicKeyCredentialRpEntity(host, 'NGINX Auth Server')
|
||||
server = U2FFido2Server(origin, rp)
|
||||
|
||||
if self.path == "/auth/get_challenge_for_new_key":
|
||||
# check whether credentials file is locked (can't be written)
|
||||
if os.access('.credentials', os.F_OK) and not os.access('.credentials', os.W_OK):
|
||||
self.send_response(403)
|
||||
self.end_headers()
|
||||
return
|
||||
|
||||
registration_data, state = server.register_begin({ 'id': b'default', 'name': "Default user", 'displayName': "Default user" })
|
||||
registration_data["publicKey"]["challenge"] = str(base64.b64encode(registration_data["publicKey"]["challenge"]), 'utf-8')
|
||||
registration_data["publicKey"]["user"]["id"] = str(base64.b64encode(registration_data["publicKey"]["user"]["id"]), 'utf-8')
|
||||
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'application/json')
|
||||
self.end_headers()
|
||||
# Save this challenge to a file so you can kill the host to add the client via CLI
|
||||
with open('data/lastchallenge', 'w') as f:
|
||||
f.write(json.dumps(state))
|
||||
self.wfile.write(bytes(json.dumps(registration_data), 'UTF-8'))
|
||||
return
|
||||
|
||||
creds = []
|
||||
try:
|
||||
with open('data/credentials', 'r', encoding='utf8') as f:
|
||||
for cred_b64 in json.load(f):
|
||||
cred, _ = AttestedCredentialData.unpack_from(base64.b64decode(cred_b64))
|
||||
creds.append(cred)
|
||||
except:
|
||||
self.send_response(401)
|
||||
self.send_header('Content-type', 'application/json')
|
||||
self.end_headers()
|
||||
self.wfile.write(bytes(json.dumps({'error': 'not_configured'}), 'UTF-8'))
|
||||
return
|
||||
|
||||
if self.path == "/auth/get_challenge_for_existing_key":
|
||||
auth_data, state = server.authenticate_begin(creds)
|
||||
auth_data["publicKey"]["challenge"] = str(base64.b64encode(auth_data["publicKey"]["challenge"]), 'utf-8')
|
||||
for i in range(len(auth_data["publicKey"]["allowCredentials"])):
|
||||
auth_data["publicKey"]["allowCredentials"][i]["id"] = base64.b64encode(auth_data["publicKey"]["allowCredentials"][i]["id"]).decode('utf8')
|
||||
|
||||
CHALLENGE.update(state)
|
||||
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'application/json')
|
||||
self.end_headers()
|
||||
self.wfile.write(bytes(json.dumps(auth_data), 'UTF-8'))
|
||||
|
||||
if self.path == "/auth/complete_challenge_for_existing_key":
|
||||
data = json.loads(self.rfile.read(int(self.headers.get('Content-Length'))))
|
||||
|
||||
credential_id = base64.b64decode(data['id'])
|
||||
client_data = ClientData(base64.b64decode(data['clientDataJSON']))
|
||||
auth_data = AuthenticatorData(base64.b64decode(data['authenticatorData']))
|
||||
signature = base64.b64decode(data['signature'])
|
||||
|
||||
server.authenticate_complete(
|
||||
CHALLENGE,
|
||||
creds,
|
||||
credential_id,
|
||||
client_data,
|
||||
auth_data,
|
||||
signature
|
||||
)
|
||||
|
||||
cookie = http.cookies.SimpleCookie()
|
||||
cookie["__Secure-Token"] = TOKEN_MANAGER.generate()
|
||||
cookie["__Secure-Token"]["path"] = "/"
|
||||
cookie["__Secure-Token"]["secure"] = True
|
||||
cookie["__Secure-Token"]["httponly"] = True
|
||||
cookie["__Secure-Token"]["samesite"] = 'None'
|
||||
cookie["__Secure-Token"]["domain"] = 'rdsm.ca'
|
||||
cookie["__Secure-Token"]["max-age"] = TOKEN_LIFETIME
|
||||
|
||||
self.send_response(200)
|
||||
self.send_header('Set-Cookie', cookie.output(header=''))
|
||||
self.end_headers()
|
||||
self.wfile.write(bytes(json.dumps({'status': 'ok'}), 'UTF-8'))
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
def run_server():
|
||||
socketserver.TCPServer.allow_reuse_address = True
|
||||
httpd = socketserver.TCPServer(("", PORT), AuthHandler)
|
||||
try:
|
||||
httpd.serve_forever()
|
||||
finally:
|
||||
httpd.server_close()
|
||||
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "save-client":
|
||||
host = sys.argv[2]
|
||||
client_data = ClientData(base64.b64decode(sys.argv[3]))
|
||||
attestation_object = AttestationObject(base64.b64decode(sys.argv[4]))
|
||||
|
||||
rp = PublicKeyCredentialRpEntity(host, 'NGINX Auth Server')
|
||||
server = U2FFido2Server('https://' + host, rp)
|
||||
|
||||
with open('data/lastchallenge') as f:
|
||||
auth_data = server.register_complete(json.loads(f.read()), client_data, attestation_object)
|
||||
cred = base64.b64encode(auth_data.credential_data).decode('utf8')
|
||||
with open('data/credentials', 'a+', encoding='utf8') as f:
|
||||
f.seek(0)
|
||||
try:
|
||||
creds = json.load(f)
|
||||
except JSONDecodeError:
|
||||
creds = []
|
||||
print("Created new credentials file")
|
||||
if cred not in creds:
|
||||
creds.append(cred)
|
||||
f.truncate(0)
|
||||
json.dump(creds, f)
|
||||
print("Credentials saved successfully")
|
||||
else:
|
||||
print("Credentials already in database")
|
||||
|
||||
elif len(sys.argv) == 2 and sys.argv[1] == '-d':
|
||||
# starts the server non-daemonized
|
||||
run_server()
|
||||
|
||||
else:
|
||||
from daemon import DaemonContext
|
||||
from sys import stdout, stderr, exit
|
||||
from lockfile import FileLock
|
||||
from signal import SIGTERM, SIGTSTP
|
||||
|
||||
def shutdown(signum, frame):
|
||||
exit(0)
|
||||
|
||||
with DaemonContext(
|
||||
chroot_directory=None,
|
||||
working_directory='/opt/app',
|
||||
stdout=stdout,
|
||||
stderr=stderr,
|
||||
pidfile=FileLock('/var/run/webauthn/webauthn.pid'),
|
||||
signal_map={
|
||||
SIGTERM: shutdown,
|
||||
SIGTSTP: shutdown
|
||||
}):
|
||||
run_server()
|
|
@ -1,2 +0,0 @@
|
|||
fido2==0.8.1
|
||||
python-daemon
|
4
run.sh
4
run.sh
|
@ -1,4 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
. venv/bin/activate
|
||||
exec python main.py -d
|
323
src/.editorconfig
Normal file
323
src/.editorconfig
Normal file
|
@ -0,0 +1,323 @@
|
|||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Don't use tabs for indentation.
|
||||
[*]
|
||||
end_of_line = lf
|
||||
indent_style = space
|
||||
# (Please don't specify an indent_size here; that has too many unintended consequences.)
|
||||
|
||||
# Code files
|
||||
[*.{cs,csx,vb,vbx}]
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
charset = utf-8-bom
|
||||
|
||||
# XML project files
|
||||
[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}]
|
||||
indent_size = 2
|
||||
|
||||
# XML config files
|
||||
[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}]
|
||||
indent_size = 2
|
||||
|
||||
# JSON files
|
||||
[*.json]
|
||||
indent_size = 2
|
||||
|
||||
# Powershell files
|
||||
[*.ps1]
|
||||
indent_size = 2
|
||||
|
||||
# Shell script files
|
||||
[*.sh]
|
||||
indent_size = 2
|
||||
|
||||
# Dotnet code style settings:
|
||||
[*.{cs,vb}]
|
||||
|
||||
# IDE0055: Fix formatting
|
||||
dotnet_diagnostic.IDE0055.severity = warning
|
||||
|
||||
# CA1848: Use LoggerDelegates
|
||||
dotnet_diagnostic.CA1848.severity = none
|
||||
|
||||
# Sort using and Import directives with System.* appearing first
|
||||
dotnet_sort_system_directives_first = true
|
||||
dotnet_separate_import_directive_groups = false
|
||||
# Avoid "this." and "Me." if not necessary
|
||||
dotnet_style_qualification_for_field = false:refactoring
|
||||
dotnet_style_qualification_for_property = false:refactoring
|
||||
dotnet_style_qualification_for_method = false:refactoring
|
||||
dotnet_style_qualification_for_event = false:refactoring
|
||||
|
||||
# Use language keywords instead of framework type names for type references
|
||||
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
|
||||
dotnet_style_predefined_type_for_member_access = true:suggestion
|
||||
|
||||
# Suggest more modern language features when available
|
||||
dotnet_style_object_initializer = true:suggestion
|
||||
dotnet_style_collection_initializer = true:suggestion
|
||||
dotnet_style_coalesce_expression = true:suggestion
|
||||
dotnet_style_null_propagation = true:suggestion
|
||||
dotnet_style_explicit_tuple_names = true:suggestion
|
||||
|
||||
# Whitespace options
|
||||
dotnet_style_allow_multiple_blank_lines_experimental = false
|
||||
|
||||
# Non-private static fields are PascalCase
|
||||
dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = suggestion
|
||||
dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields
|
||||
dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style
|
||||
|
||||
dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field
|
||||
dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected
|
||||
dotnet_naming_symbols.non_private_static_fields.required_modifiers = static
|
||||
|
||||
dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case
|
||||
|
||||
# Non-private readonly fields are PascalCase
|
||||
dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.severity = suggestion
|
||||
dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.symbols = non_private_readonly_fields
|
||||
dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.style = non_private_readonly_field_style
|
||||
|
||||
dotnet_naming_symbols.non_private_readonly_fields.applicable_kinds = field
|
||||
dotnet_naming_symbols.non_private_readonly_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected
|
||||
dotnet_naming_symbols.non_private_readonly_fields.required_modifiers = readonly
|
||||
|
||||
dotnet_naming_style.non_private_readonly_field_style.capitalization = pascal_case
|
||||
|
||||
# Constants are PascalCase
|
||||
dotnet_naming_rule.constants_should_be_pascal_case.severity = suggestion
|
||||
dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants
|
||||
dotnet_naming_rule.constants_should_be_pascal_case.style = constant_style
|
||||
|
||||
dotnet_naming_symbols.constants.applicable_kinds = field, local
|
||||
dotnet_naming_symbols.constants.required_modifiers = const
|
||||
|
||||
dotnet_naming_style.constant_style.capitalization = pascal_case
|
||||
|
||||
# Static fields are camelCase and start with s_
|
||||
dotnet_naming_rule.static_fields_should_be_camel_case.severity = suggestion
|
||||
dotnet_naming_rule.static_fields_should_be_camel_case.symbols = static_fields
|
||||
dotnet_naming_rule.static_fields_should_be_camel_case.style = static_field_style
|
||||
|
||||
dotnet_naming_symbols.static_fields.applicable_kinds = field
|
||||
dotnet_naming_symbols.static_fields.required_modifiers = static
|
||||
|
||||
dotnet_naming_style.static_field_style.capitalization = camel_case
|
||||
dotnet_naming_style.static_field_style.required_prefix = s_
|
||||
|
||||
# Instance fields are camelCase and start with _
|
||||
dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion
|
||||
dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields
|
||||
dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style
|
||||
|
||||
dotnet_naming_symbols.instance_fields.applicable_kinds = field
|
||||
|
||||
dotnet_naming_style.instance_field_style.capitalization = camel_case
|
||||
dotnet_naming_style.instance_field_style.required_prefix = _
|
||||
|
||||
# Locals and parameters are camelCase
|
||||
dotnet_naming_rule.locals_should_be_camel_case.severity = suggestion
|
||||
dotnet_naming_rule.locals_should_be_camel_case.symbols = locals_and_parameters
|
||||
dotnet_naming_rule.locals_should_be_camel_case.style = camel_case_style
|
||||
|
||||
dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local
|
||||
|
||||
dotnet_naming_style.camel_case_style.capitalization = camel_case
|
||||
|
||||
# Local functions are PascalCase
|
||||
dotnet_naming_rule.local_functions_should_be_pascal_case.severity = suggestion
|
||||
dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions
|
||||
dotnet_naming_rule.local_functions_should_be_pascal_case.style = local_function_style
|
||||
|
||||
dotnet_naming_symbols.local_functions.applicable_kinds = local_function
|
||||
|
||||
dotnet_naming_style.local_function_style.capitalization = pascal_case
|
||||
|
||||
# By default, name items with PascalCase
|
||||
dotnet_naming_rule.members_should_be_pascal_case.severity = suggestion
|
||||
dotnet_naming_rule.members_should_be_pascal_case.symbols = all_members
|
||||
dotnet_naming_rule.members_should_be_pascal_case.style = pascal_case_style
|
||||
|
||||
dotnet_naming_symbols.all_members.applicable_kinds = *
|
||||
|
||||
dotnet_naming_style.pascal_case_style.capitalization = pascal_case
|
||||
|
||||
# error RS2008: Enable analyzer release tracking for the analyzer project containing rule '{0}'
|
||||
dotnet_diagnostic.RS2008.severity = none
|
||||
|
||||
# IDE0073: File header
|
||||
dotnet_diagnostic.IDE0073.severity = none
|
||||
file_header_template =
|
||||
|
||||
# IDE0035: Remove unreachable code
|
||||
dotnet_diagnostic.IDE0035.severity = warning
|
||||
|
||||
# IDE0036: Order modifiers
|
||||
dotnet_diagnostic.IDE0036.severity = warning
|
||||
|
||||
# IDE0043: Format string contains invalid placeholder
|
||||
dotnet_diagnostic.IDE0043.severity = warning
|
||||
|
||||
# IDE0044: Make field readonly
|
||||
dotnet_diagnostic.IDE0044.severity = warning
|
||||
|
||||
# RS0016: Only enable if API files are present
|
||||
dotnet_public_api_analyzer.require_api_files = true
|
||||
|
||||
# CSharp code style settings:
|
||||
[*.cs]
|
||||
|
||||
# enforce file scoped namespaces
|
||||
csharp_style_namespace_declarations = file_scoped:warning
|
||||
|
||||
# Newline settings
|
||||
csharp_new_line_before_open_brace = none
|
||||
csharp_new_line_before_else = false
|
||||
csharp_new_line_before_catch = false
|
||||
csharp_new_line_before_finally = false
|
||||
csharp_new_line_before_members_in_object_initializers = true
|
||||
csharp_new_line_before_members_in_anonymous_types = true
|
||||
csharp_new_line_between_query_expression_clauses = true
|
||||
|
||||
# Indentation preferences
|
||||
csharp_indent_block_contents = true
|
||||
csharp_indent_braces = false
|
||||
csharp_indent_case_contents = true
|
||||
csharp_indent_case_contents_when_block = false
|
||||
csharp_indent_switch_labels = true
|
||||
csharp_indent_labels = flush_left
|
||||
|
||||
# Whitespace options
|
||||
csharp_style_allow_embedded_statements_on_same_line_experimental = true
|
||||
csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false
|
||||
csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = false
|
||||
|
||||
# Prefer "var" everywhere
|
||||
csharp_style_var_for_built_in_types = true:suggestion
|
||||
csharp_style_var_when_type_is_apparent = true:suggestion
|
||||
csharp_style_var_elsewhere = true:suggestion
|
||||
|
||||
# Prefer method-like constructs to have a block body
|
||||
csharp_style_expression_bodied_methods = false:none
|
||||
csharp_style_expression_bodied_constructors = false:none
|
||||
csharp_style_expression_bodied_operators = false:none
|
||||
|
||||
# Prefer property-like constructs to have an expression-body
|
||||
csharp_style_expression_bodied_properties = true:none
|
||||
csharp_style_expression_bodied_indexers = true:none
|
||||
csharp_style_expression_bodied_accessors = true:none
|
||||
|
||||
# Suggest more modern language features when available
|
||||
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
|
||||
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
|
||||
csharp_style_inlined_variable_declaration = true:suggestion
|
||||
csharp_style_throw_expression = true:suggestion
|
||||
csharp_style_conditional_delegate_call = true:suggestion
|
||||
|
||||
# Space preferences
|
||||
csharp_space_after_cast = false
|
||||
csharp_space_after_colon_in_inheritance_clause = true
|
||||
csharp_space_after_comma = true
|
||||
csharp_space_after_dot = false
|
||||
csharp_space_after_keywords_in_control_flow_statements = true
|
||||
csharp_space_after_semicolon_in_for_statement = true
|
||||
csharp_space_around_binary_operators = before_and_after
|
||||
csharp_space_around_declaration_statements = do_not_ignore
|
||||
csharp_space_before_colon_in_inheritance_clause = true
|
||||
csharp_space_before_comma = false
|
||||
csharp_space_before_dot = false
|
||||
csharp_space_before_open_square_brackets = false
|
||||
csharp_space_before_semicolon_in_for_statement = false
|
||||
csharp_space_between_empty_square_brackets = false
|
||||
csharp_space_between_method_call_empty_parameter_list_parentheses = false
|
||||
csharp_space_between_method_call_name_and_opening_parenthesis = false
|
||||
csharp_space_between_method_call_parameter_list_parentheses = false
|
||||
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
|
||||
csharp_space_between_method_declaration_name_and_open_parenthesis = false
|
||||
csharp_space_between_method_declaration_parameter_list_parentheses = false
|
||||
csharp_space_between_parentheses = false
|
||||
csharp_space_between_square_brackets = false
|
||||
|
||||
# Blocks are allowed
|
||||
csharp_prefer_braces = when_multiline:warning
|
||||
csharp_preserve_single_line_blocks = true
|
||||
csharp_preserve_single_line_statements = true
|
||||
|
||||
# Currently only enabled for C# due to crash in VB analyzer. VB can be enabled once
|
||||
# https://github.com/dotnet/roslyn/pull/54259 has been published.
|
||||
dotnet_style_allow_statement_immediately_after_block_experimental = false
|
||||
|
||||
# ignore unused value expression message
|
||||
csharp_style_unused_value_expression_statement_preference = discard_variable:none
|
||||
|
||||
# allow underscores in our enums
|
||||
dotnet_diagnostic.CA1707.severity = none
|
||||
|
||||
[OOT.API/Models/Contract/**.cs]
|
||||
# suppress analyzers in generated code
|
||||
dotnet_analyzer_diagnostic.severity = none
|
||||
|
||||
[src/CodeStyle/**.{cs,vb}]
|
||||
# warning RS0005: Do not use generic CodeAction.Create to create CodeAction
|
||||
dotnet_diagnostic.RS0005.severity = none
|
||||
|
||||
[src/{Analyzers,CodeStyle,Features,Workspaces,EditorFeatures,VisualStudio}/**/*.{cs,vb}]
|
||||
|
||||
# IDE0011: Add braces
|
||||
csharp_prefer_braces = when_multiline:warning
|
||||
# NOTE: We need the below severity entry for Add Braces due to https://github.com/dotnet/roslyn/issues/44201
|
||||
dotnet_diagnostic.IDE0011.severity = warning
|
||||
|
||||
# IDE0040: Add accessibility modifiers
|
||||
dotnet_diagnostic.IDE0040.severity = warning
|
||||
|
||||
# CONSIDER: Are IDE0051 and IDE0052 too noisy to be warnings for IDE editing scenarios? Should they be made build-only warnings?
|
||||
# IDE0051: Remove unused private member
|
||||
dotnet_diagnostic.IDE0051.severity = warning
|
||||
|
||||
# IDE0052: Remove unread private member
|
||||
dotnet_diagnostic.IDE0052.severity = warning
|
||||
|
||||
# IDE0059: Unnecessary assignment to a value
|
||||
dotnet_diagnostic.IDE0059.severity = warning
|
||||
|
||||
# IDE0060: Remove unused parameter
|
||||
dotnet_diagnostic.IDE0060.severity = warning
|
||||
|
||||
# CA1012: Abstract types should not have public constructors
|
||||
dotnet_diagnostic.CA1012.severity = warning
|
||||
|
||||
# CA1822: Make member static
|
||||
dotnet_diagnostic.CA1822.severity = warning
|
||||
|
||||
# Prefer "var" everywhere
|
||||
dotnet_diagnostic.IDE0007.severity = warning
|
||||
csharp_style_var_for_built_in_types = true:warning
|
||||
csharp_style_var_when_type_is_apparent = true:warning
|
||||
csharp_style_var_elsewhere = true:warning
|
||||
|
||||
# dotnet_style_allow_multiple_blank_lines_experimental
|
||||
dotnet_diagnostic.IDE2000.severity = warning
|
||||
|
||||
# csharp_style_allow_embedded_statements_on_same_line_experimental
|
||||
dotnet_diagnostic.IDE2001.severity = warning
|
||||
|
||||
# csharp_style_allow_blank_lines_between_consecutive_braces_experimental
|
||||
dotnet_diagnostic.IDE2002.severity = warning
|
||||
|
||||
# dotnet_style_allow_statement_immediately_after_block_experimental
|
||||
dotnet_diagnostic.IDE2003.severity = warning
|
||||
|
||||
# csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental
|
||||
dotnet_diagnostic.IDE2004.severity = warning
|
||||
|
||||
[src/{VisualStudio}/**/*.{cs,vb}]
|
||||
# CA1822: Make member static
|
||||
# Not enforced as a build 'warning' for 'VisualStudio' layer due to large number of false positives from https://github.com/dotnet/roslyn-analyzers/issues/3857 and https://github.com/dotnet/roslyn-analyzers/issues/3858
|
||||
# Additionally, there is a risk of accidentally breaking an internal API that partners rely on though IVT.
|
||||
dotnet_diagnostic.CA1822.severity = suggestion
|
311
src/Program.cs
Normal file
311
src/Program.cs
Normal file
|
@ -0,0 +1,311 @@
|
|||
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; }
|
||||
}
|
||||
|
||||
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") ?? "localhost";
|
||||
private static readonly string s_db = Environment
|
||||
.GetEnvironmentVariable("WEBAUTHN_DB") ?? "credentials.db";
|
||||
private static readonly long s_lifetime = long.Parse(Environment
|
||||
.GetEnvironmentVariable("WEBAUTHN_LIFETIME") ?? "7",
|
||||
CultureInfo.InvariantCulture) * 60 * 60 * 24;
|
||||
private static readonly string? s_password = Environment
|
||||
.GetEnvironmentVariable("WEBAUTHN_PASSWORD");
|
||||
private static readonly int s_port = int.Parse(Environment
|
||||
.GetEnvironmentVariable("WEBAUTHN_PORT") ?? "5000",
|
||||
CultureInfo.InvariantCulture);
|
||||
private static readonly Fido2 s_fido2 = new(new Fido2Configuration {
|
||||
ServerDomain = s_domain,
|
||||
ServerName = "WebauthnProxy",
|
||||
Origins = new(new[] { $"http{(
|
||||
s_domain == "localhost"
|
||||
? string.Empty
|
||||
: "s")}://{s_domain}{(
|
||||
s_domain == "localhost"
|
||||
? $":{s_port}"
|
||||
: string.Empty)}" }),
|
||||
});
|
||||
private static readonly List<PublicKeyCredentialDescriptor> s_keys = new();
|
||||
|
||||
private static string ConnectionString { get => $"Data Source={s_db}"; }
|
||||
|
||||
public static void Main(string[] args) {
|
||||
var app = Initialize(args);
|
||||
app.UseSession();
|
||||
|
||||
app.MapGet("/favicon.ico", () => Results.File(Convert.FromBase64String(
|
||||
$"AAABAAEAEBAAAAAAAABoBQAAFgAAACgAAAAQAAAAIAAAAAEACAAAAAAAAAEAAAAAAAAAAAAAAAEAAAAAAAD///8{new string('A', 1788)}="),
|
||||
contentType: "image/x-icon"));
|
||||
|
||||
app.MapGet("/auth/check", async (context) => {
|
||||
var token = context.Request.Cookies[COOKIE_NAME];
|
||||
Console.WriteLine(token);
|
||||
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;
|
||||
|
||||
context.Response.Cookies.Append(COOKIE_NAME, string.Empty, new CookieOptions {
|
||||
Path = "/",
|
||||
Secure = true,
|
||||
HttpOnly = true,
|
||||
MaxAge = TimeSpan.Zero,
|
||||
Domain = s_domain,
|
||||
});
|
||||
|
||||
context.Response.Redirect("/");
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
app.MapPost("/auth/key", async (context) => {
|
||||
var options = s_fido2.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;
|
||||
}
|
||||
}
|
||||
|
||||
var res = await s_fido2.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;
|
||||
}
|
||||
|
||||
context.Response.Cookies.Append(
|
||||
COOKIE_NAME,
|
||||
GenerateToken(connection),
|
||||
new CookieOptions {
|
||||
Path = "/",
|
||||
Secure = true,
|
||||
HttpOnly = true,
|
||||
SameSite = SameSiteMode.None,
|
||||
Domain = s_domain,
|
||||
MaxAge = TimeSpan.FromSeconds(s_lifetime),
|
||||
});
|
||||
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 = s_fido2.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;
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(s_password)
|
||||
|| s_password != req.Password) {
|
||||
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 s_fido2.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();
|
||||
});
|
||||
|
||||
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='credentials'",
|
||||
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();
|
||||
|
||||
// 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();
|
||||
return builder.Build();
|
||||
}
|
||||
}
|
102
src/login.html
Normal file
102
src/login.html
Normal file
|
@ -0,0 +1,102 @@
|
|||
<!doctype html>
|
||||
<html lang='en'>
|
||||
<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>
|
||||
</body>
|
||||
</html>
|
8
src/omnisharp.json
Normal file
8
src/omnisharp.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"RoslynExtensionsOptions": {
|
||||
"enableAnalyzersSupport": true
|
||||
},
|
||||
"FormattingOptions": {
|
||||
"enableEditorConfigSupport": true
|
||||
}
|
||||
}
|
10
src/schema.sql
Normal file
10
src/schema.sql
Normal file
|
@ -0,0 +1,10 @@
|
|||
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
|
||||
);
|
24
src/webauthn-proxy.csproj
Normal file
24
src/webauthn-proxy.csproj
Normal file
|
@ -0,0 +1,24 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.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" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="schema.sql">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="login.html">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
Loading…
Reference in a new issue