mirror of
https://github.com/healthchecks/healthchecks.git
synced 2025-04-06 21:58:48 +00:00
Refactor webauthn implementation, use webauthn-json
This commit is contained in:
parent
64a6245736
commit
57021e962c
8 changed files with 356 additions and 135 deletions
hc
static/js
templates/accounts
|
@ -1,9 +1,6 @@
|
||||||
import base64
|
|
||||||
import binascii
|
|
||||||
from datetime import timedelta as td
|
from datetime import timedelta as td
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.contrib.auth import authenticate
|
from django.contrib.auth import authenticate
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from hc.accounts.models import REPORT_CHOICES, Member
|
from hc.accounts.models import REPORT_CHOICES, Member
|
||||||
|
@ -17,17 +14,6 @@ class LowercaseEmailField(forms.EmailField):
|
||||||
return value.lower()
|
return value.lower()
|
||||||
|
|
||||||
|
|
||||||
class Base64Field(forms.CharField):
|
|
||||||
def to_python(self, value):
|
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
return base64.b64decode(value.encode())
|
|
||||||
except binascii.Error:
|
|
||||||
raise ValidationError(message="Cannot decode base64")
|
|
||||||
|
|
||||||
|
|
||||||
class SignupForm(forms.Form):
|
class SignupForm(forms.Form):
|
||||||
# Call it "identity" instead of "email"
|
# Call it "identity" instead of "email"
|
||||||
# to avoid some of the dumber bots
|
# to avoid some of the dumber bots
|
||||||
|
@ -153,15 +139,11 @@ class TransferForm(forms.Form):
|
||||||
|
|
||||||
class AddWebAuthnForm(forms.Form):
|
class AddWebAuthnForm(forms.Form):
|
||||||
name = forms.CharField(max_length=100)
|
name = forms.CharField(max_length=100)
|
||||||
client_data_json = Base64Field()
|
response = forms.CharField()
|
||||||
attestation_object = Base64Field()
|
|
||||||
|
|
||||||
|
|
||||||
class WebAuthnForm(forms.Form):
|
class WebAuthnForm(forms.Form):
|
||||||
credential_id = Base64Field()
|
response = forms.CharField()
|
||||||
client_data_json = Base64Field()
|
|
||||||
authenticator_data = Base64Field()
|
|
||||||
signature = Base64Field()
|
|
||||||
|
|
||||||
|
|
||||||
class TotpForm(forms.Form):
|
class TotpForm(forms.Form):
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import base64
|
|
||||||
from datetime import timedelta as td
|
from datetime import timedelta as td
|
||||||
from secrets import token_bytes, token_urlsafe
|
from secrets import token_urlsafe
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
|
@ -20,16 +19,12 @@ from django.utils.timezone import now
|
||||||
from django.urls import resolve, reverse, Resolver404
|
from django.urls import resolve, reverse, Resolver404
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from django.views.decorators.http import require_POST
|
from django.views.decorators.http import require_POST
|
||||||
from fido2.ctap2 import AttestationObject, AuthenticatorData
|
|
||||||
from fido2.client import ClientData
|
|
||||||
from fido2.server import Fido2Server
|
|
||||||
from fido2.webauthn import PublicKeyCredentialRpEntity
|
|
||||||
from fido2 import cbor
|
|
||||||
from hc.accounts import forms
|
from hc.accounts import forms
|
||||||
from hc.accounts.decorators import require_sudo_mode
|
from hc.accounts.decorators import require_sudo_mode
|
||||||
from hc.accounts.models import Credential, Profile, Project, Member
|
from hc.accounts.models import Credential, Profile, Project, Member
|
||||||
from hc.api.models import Channel, Check, TokenBucket
|
from hc.api.models import Channel, Check, TokenBucket
|
||||||
from hc.payments.models import Subscription
|
from hc.payments.models import Subscription
|
||||||
|
from hc.lib.webauthn import Server
|
||||||
import pyotp
|
import pyotp
|
||||||
import segno
|
import segno
|
||||||
|
|
||||||
|
@ -46,7 +41,7 @@ POST_LOGIN_ROUTES = (
|
||||||
"hc-uncloak",
|
"hc-uncloak",
|
||||||
)
|
)
|
||||||
|
|
||||||
FIDO2_SERVER = Fido2Server(PublicKeyCredentialRpEntity(settings.RP_ID, "healthchecks"))
|
WEBAUTHN = Server(settings.RP_ID, "healthchecks")
|
||||||
|
|
||||||
|
|
||||||
def _allow_redirect(redirect_url):
|
def _allow_redirect(redirect_url):
|
||||||
|
@ -667,27 +662,6 @@ def remove_project(request, code):
|
||||||
return redirect("hc-index")
|
return redirect("hc-index")
|
||||||
|
|
||||||
|
|
||||||
def _get_credential_data(request, form):
|
|
||||||
"""Complete WebAuthn registration, return binary credential data.
|
|
||||||
|
|
||||||
This function is an interface to the fido2 library. It is separated
|
|
||||||
out so that we don't need to mock ClientData, AttestationObject,
|
|
||||||
register_complete separately in tests.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
auth_data = FIDO2_SERVER.register_complete(
|
|
||||||
request.session["state"],
|
|
||||||
ClientData(form.cleaned_data["client_data_json"]),
|
|
||||||
AttestationObject(form.cleaned_data["attestation_object"]),
|
|
||||||
)
|
|
||||||
except ValueError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return auth_data.credential_data
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@require_sudo_mode
|
@require_sudo_mode
|
||||||
def add_webauthn(request):
|
def add_webauthn(request):
|
||||||
|
@ -699,42 +673,25 @@ def add_webauthn(request):
|
||||||
if not form.is_valid():
|
if not form.is_valid():
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
credential_data = _get_credential_data(request, form)
|
try:
|
||||||
if not credential_data:
|
auth_data = WEBAUTHN.register_complete(
|
||||||
|
request.session["state"], form.cleaned_data["response"]
|
||||||
|
)
|
||||||
|
except ValueError as e:
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
c = Credential(user=request.user)
|
c = Credential(user=request.user)
|
||||||
c.name = form.cleaned_data["name"]
|
c.name = form.cleaned_data["name"]
|
||||||
c.data = credential_data
|
c.data = auth_data.credential_data
|
||||||
c.save()
|
c.save()
|
||||||
|
|
||||||
request.session["added_credential_name"] = c.name
|
request.session["added_credential_name"] = c.name
|
||||||
return redirect("hc-profile")
|
return redirect("hc-profile")
|
||||||
|
|
||||||
credentials = [c.unpack() for c in request.user.credentials.all()]
|
credentials = [c.unpack() for c in request.user.credentials.all()]
|
||||||
# User handle is used in a username-less authentication, to map a credential
|
options, state = WEBAUTHN.register_begin(request.user.email, credentials)
|
||||||
# received from browser with an user account in the database.
|
|
||||||
# Since we only use security keys as a second factor,
|
|
||||||
# the user handle is not of much use to us.
|
|
||||||
#
|
|
||||||
# The user handle:
|
|
||||||
# - must not be blank,
|
|
||||||
# - must not be a constant value,
|
|
||||||
# - must not contain personally identifiable information.
|
|
||||||
# So we use random bytes, and don't store them on our end:
|
|
||||||
options, state = FIDO2_SERVER.register_begin(
|
|
||||||
{
|
|
||||||
"id": token_bytes(16),
|
|
||||||
"name": request.user.email,
|
|
||||||
"displayName": request.user.email,
|
|
||||||
},
|
|
||||||
credentials,
|
|
||||||
)
|
|
||||||
|
|
||||||
request.session["state"] = state
|
request.session["state"] = state
|
||||||
|
return render(request, "accounts/add_credential.html", {"options": options})
|
||||||
ctx = {"options": base64.b64encode(cbor.encode(options)).decode()}
|
|
||||||
return render(request, "accounts/add_credential.html", ctx)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
@ -811,30 +768,6 @@ def remove_credential(request, code):
|
||||||
return render(request, "accounts/remove_credential.html", ctx)
|
return render(request, "accounts/remove_credential.html", ctx)
|
||||||
|
|
||||||
|
|
||||||
def _check_credential(request, form, credentials):
|
|
||||||
"""Complete WebAuthn authentication, return True on success.
|
|
||||||
|
|
||||||
This function is an interface to the fido2 library. It is separated
|
|
||||||
out so that we don't need to mock ClientData, AuthenticatorData,
|
|
||||||
authenticate_complete separately in tests.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
FIDO2_SERVER.authenticate_complete(
|
|
||||||
request.session["state"],
|
|
||||||
credentials,
|
|
||||||
form.cleaned_data["credential_id"],
|
|
||||||
ClientData(form.cleaned_data["client_data_json"]),
|
|
||||||
AuthenticatorData(form.cleaned_data["authenticator_data"]),
|
|
||||||
form.cleaned_data["signature"],
|
|
||||||
)
|
|
||||||
except ValueError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def login_webauthn(request):
|
def login_webauthn(request):
|
||||||
# We require RP_ID. Fail predicably if it is not set:
|
# We require RP_ID. Fail predicably if it is not set:
|
||||||
if not settings.RP_ID:
|
if not settings.RP_ID:
|
||||||
|
@ -863,7 +796,13 @@ def login_webauthn(request):
|
||||||
if not form.is_valid():
|
if not form.is_valid():
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
if not _check_credential(request, form, credentials):
|
try:
|
||||||
|
WEBAUTHN.authenticate_complete(
|
||||||
|
request.session["state"],
|
||||||
|
credentials,
|
||||||
|
form.cleaned_data["response"],
|
||||||
|
)
|
||||||
|
except ValueError as e:
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
request.session.pop("state")
|
request.session.pop("state")
|
||||||
|
@ -871,7 +810,7 @@ def login_webauthn(request):
|
||||||
auth_login(request, user, "hc.accounts.backends.EmailBackend")
|
auth_login(request, user, "hc.accounts.backends.EmailBackend")
|
||||||
return _redirect_after_login(request)
|
return _redirect_after_login(request)
|
||||||
|
|
||||||
options, state = FIDO2_SERVER.authenticate_begin(credentials)
|
options, state = WEBAUTHN.authenticate_begin(credentials)
|
||||||
request.session["state"] = state
|
request.session["state"] = state
|
||||||
|
|
||||||
totp_url = None
|
totp_url = None
|
||||||
|
@ -882,7 +821,7 @@ def login_webauthn(request):
|
||||||
totp_url += "?next=%s" % redirect_url
|
totp_url += "?next=%s" % redirect_url
|
||||||
|
|
||||||
ctx = {
|
ctx = {
|
||||||
"options": base64.b64encode(cbor.encode(options)).decode(),
|
"options": options,
|
||||||
"totp_url": totp_url,
|
"totp_url": totp_url,
|
||||||
}
|
}
|
||||||
return render(request, "accounts/login_webauthn.html", ctx)
|
return render(request, "accounts/login_webauthn.html", ctx)
|
||||||
|
|
86
hc/lib/webauthn.py
Normal file
86
hc/lib/webauthn.py
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
import json
|
||||||
|
from secrets import token_bytes
|
||||||
|
|
||||||
|
from fido2.client import ClientData
|
||||||
|
from fido2.ctap2 import AttestationObject, AuthenticatorData
|
||||||
|
from fido2.server import Fido2Server
|
||||||
|
from fido2.utils import websafe_encode, websafe_decode
|
||||||
|
from fido2.webauthn import PublicKeyCredentialRpEntity
|
||||||
|
|
||||||
|
|
||||||
|
def bytes_to_b64(obj):
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
return {k: bytes_to_b64(v) for k, v in obj.items()}
|
||||||
|
|
||||||
|
if isinstance(obj, list):
|
||||||
|
return [bytes_to_b64(v) for v in obj]
|
||||||
|
|
||||||
|
if isinstance(obj, bytes):
|
||||||
|
return websafe_encode(obj)
|
||||||
|
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
json_decode_map = {
|
||||||
|
"clientDataJSON": ClientData,
|
||||||
|
"attestationObject": AttestationObject,
|
||||||
|
"rawId": bytes,
|
||||||
|
"authenticatorData": AuthenticatorData,
|
||||||
|
"signature": bytes,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def json_decode_hook(d):
|
||||||
|
for key, cls in json_decode_map.items():
|
||||||
|
if key in d:
|
||||||
|
as_bytes = websafe_decode(d[key])
|
||||||
|
d[key] = cls(as_bytes)
|
||||||
|
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
class Server(object):
|
||||||
|
def __init__(self, id, name):
|
||||||
|
self.server = Fido2Server(PublicKeyCredentialRpEntity(id, name))
|
||||||
|
|
||||||
|
def register_begin(self, email, credentials):
|
||||||
|
# User handle is used in a username-less authentication, to map a credential
|
||||||
|
# received from browser with an user account in the database.
|
||||||
|
# Since we only use security keys as a second factor,
|
||||||
|
# the user handle is not of much use to us.
|
||||||
|
#
|
||||||
|
# The user handle:
|
||||||
|
# - must not be blank,
|
||||||
|
# - must not be a constant value,
|
||||||
|
# - must not contain personally identifiable information.
|
||||||
|
# So we use random bytes, and don't store them on our end:
|
||||||
|
user = {
|
||||||
|
"id": token_bytes(16),
|
||||||
|
"name": email,
|
||||||
|
"displayName": email,
|
||||||
|
}
|
||||||
|
options, state = self.server.register_begin(user, credentials)
|
||||||
|
return bytes_to_b64(options), state
|
||||||
|
|
||||||
|
def register_complete(self, state, response_json):
|
||||||
|
doc = json.loads(response_json, object_hook=json_decode_hook)
|
||||||
|
return self.server.register_complete(
|
||||||
|
state,
|
||||||
|
doc["response"]["clientDataJSON"],
|
||||||
|
doc["response"]["attestationObject"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def authenticate_begin(self, credentials):
|
||||||
|
options, state = self.server.authenticate_begin(credentials)
|
||||||
|
return bytes_to_b64(options), state
|
||||||
|
|
||||||
|
def authenticate_complete(self, state, credentials, response_json):
|
||||||
|
doc = json.loads(response_json, object_hook=json_decode_hook)
|
||||||
|
return self.server.authenticate_complete(
|
||||||
|
state,
|
||||||
|
credentials,
|
||||||
|
doc["rawId"],
|
||||||
|
doc["response"]["clientDataJSON"],
|
||||||
|
doc["response"]["authenticatorData"],
|
||||||
|
doc["response"]["signature"],
|
||||||
|
)
|
|
@ -1,12 +1,5 @@
|
||||||
$(function() {
|
$(function() {
|
||||||
var form = document.getElementById("add-credential-form");
|
var form = document.getElementById("add-credential-form");
|
||||||
var optionsBytes = Uint8Array.from(atob(form.dataset.options), c => c.charCodeAt(0));
|
|
||||||
// cbor.js expects ArrayBuffer as input when decoding
|
|
||||||
var options = CBOR.decode(optionsBytes.buffer);
|
|
||||||
|
|
||||||
function b64(arraybuffer) {
|
|
||||||
return btoa(String.fromCharCode.apply(null, new Uint8Array(arraybuffer)));
|
|
||||||
}
|
|
||||||
|
|
||||||
function requestCredentials() {
|
function requestCredentials() {
|
||||||
// Hide error & success messages, show the "waiting" message
|
// Hide error & success messages, show the "waiting" message
|
||||||
|
@ -15,10 +8,9 @@ $(function() {
|
||||||
$("#error").addClass("hide");
|
$("#error").addClass("hide");
|
||||||
$("#success").addClass("hide");
|
$("#success").addClass("hide");
|
||||||
|
|
||||||
navigator.credentials.create(options).then(function(attestation) {
|
var options = JSON.parse($("#options").text());
|
||||||
$("#attestation_object").val(b64(attestation.response.attestationObject));
|
webauthnJSON.create(options).then(function(response) {
|
||||||
$("#client_data_json").val(b64(attestation.response.clientDataJSON));
|
$("#response").val(JSON.stringify(response));
|
||||||
|
|
||||||
// Show the success message and save button
|
// Show the success message and save button
|
||||||
$("#waiting").addClass("hide");
|
$("#waiting").addClass("hide");
|
||||||
$("#success").removeClass("hide");
|
$("#success").removeClass("hide");
|
||||||
|
|
|
@ -1,24 +1,14 @@
|
||||||
$(function() {
|
$(function() {
|
||||||
var form = document.getElementById("login-tfa-form");
|
var form = document.getElementById("login-tfa-form");
|
||||||
var optionsBytes = Uint8Array.from(atob(form.dataset.options), c => c.charCodeAt(0));
|
|
||||||
// cbor.js expects ArrayBuffer as input when decoding
|
|
||||||
var options = CBOR.decode(optionsBytes.buffer);
|
|
||||||
|
|
||||||
function b64(arraybuffer) {
|
|
||||||
return btoa(String.fromCharCode.apply(null, new Uint8Array(arraybuffer)));
|
|
||||||
}
|
|
||||||
|
|
||||||
function authenticate() {
|
function authenticate() {
|
||||||
$("#pick-method").addClass("hide");
|
$("#pick-method").addClass("hide");
|
||||||
$("#waiting").removeClass("hide");
|
$("#waiting").removeClass("hide");
|
||||||
$("#error").addClass("hide");
|
$("#error").addClass("hide");
|
||||||
|
|
||||||
navigator.credentials.get(options).then(function(assertion) {
|
var options = JSON.parse($("#options").text());
|
||||||
$("#credential_id").val(b64(assertion.rawId));
|
webauthnJSON.get(options).then(function(response) {
|
||||||
$("#authenticator_data").val(b64(assertion.response.authenticatorData));
|
$("#response").val(JSON.stringify(response));
|
||||||
$("#client_data_json").val(b64(assertion.response.clientDataJSON));
|
|
||||||
$("#signature").val(b64(assertion.response.signature));
|
|
||||||
|
|
||||||
// Show the success message and save button
|
// Show the success message and save button
|
||||||
$("#waiting").addClass("hide");
|
$("#waiting").addClass("hide");
|
||||||
$("#success").removeClass("hide");
|
$("#success").removeClass("hide");
|
||||||
|
|
232
static/js/webauthn-json.browser-global.js
Normal file
232
static/js/webauthn-json.browser-global.js
Normal file
|
@ -0,0 +1,232 @@
|
||||||
|
(() => {
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __async = (__this, __arguments, generator) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
var fulfilled = (value) => {
|
||||||
|
try {
|
||||||
|
step(generator.next(value));
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var rejected = (value) => {
|
||||||
|
try {
|
||||||
|
step(generator.throw(value));
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
|
||||||
|
step((generator = generator.apply(__this, __arguments)).next());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// src/webauthn-json/index.ts
|
||||||
|
var webauthn_json_exports = {};
|
||||||
|
__export(webauthn_json_exports, {
|
||||||
|
create: () => create,
|
||||||
|
get: () => get,
|
||||||
|
schema: () => schema,
|
||||||
|
supported: () => supported
|
||||||
|
});
|
||||||
|
|
||||||
|
// src/webauthn-json/base64url.ts
|
||||||
|
function base64urlToBuffer(baseurl64String) {
|
||||||
|
const padding = "==".slice(0, (4 - baseurl64String.length % 4) % 4);
|
||||||
|
const base64String = baseurl64String.replace(/-/g, "+").replace(/_/g, "/") + padding;
|
||||||
|
const str = atob(base64String);
|
||||||
|
const buffer = new ArrayBuffer(str.length);
|
||||||
|
const byteView = new Uint8Array(buffer);
|
||||||
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
byteView[i] = str.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
function bufferToBase64url(buffer) {
|
||||||
|
const byteView = new Uint8Array(buffer);
|
||||||
|
let str = "";
|
||||||
|
for (const charCode of byteView) {
|
||||||
|
str += String.fromCharCode(charCode);
|
||||||
|
}
|
||||||
|
const base64String = btoa(str);
|
||||||
|
const base64urlString = base64String.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
||||||
|
return base64urlString;
|
||||||
|
}
|
||||||
|
|
||||||
|
// src/webauthn-json/convert.ts
|
||||||
|
var copyValue = "copy";
|
||||||
|
var convertValue = "convert";
|
||||||
|
function convert(conversionFn, schema2, input) {
|
||||||
|
if (schema2 === copyValue) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
if (schema2 === convertValue) {
|
||||||
|
return conversionFn(input);
|
||||||
|
}
|
||||||
|
if (schema2 instanceof Array) {
|
||||||
|
return input.map((v) => convert(conversionFn, schema2[0], v));
|
||||||
|
}
|
||||||
|
if (schema2 instanceof Object) {
|
||||||
|
const output = {};
|
||||||
|
for (const [key, schemaField] of Object.entries(schema2)) {
|
||||||
|
if (schemaField.derive) {
|
||||||
|
const v = schemaField.derive(input);
|
||||||
|
if (v !== void 0) {
|
||||||
|
input[key] = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!(key in input)) {
|
||||||
|
if (schemaField.required) {
|
||||||
|
throw new Error(`Missing key: ${key}`);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (input[key] == null) {
|
||||||
|
output[key] = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
output[key] = convert(conversionFn, schemaField.schema, input[key]);
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function derived(schema2, derive) {
|
||||||
|
return {
|
||||||
|
required: true,
|
||||||
|
schema: schema2,
|
||||||
|
derive
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function required(schema2) {
|
||||||
|
return {
|
||||||
|
required: true,
|
||||||
|
schema: schema2
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function optional(schema2) {
|
||||||
|
return {
|
||||||
|
required: false,
|
||||||
|
schema: schema2
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// src/webauthn-json/basic/schema.ts
|
||||||
|
var publicKeyCredentialDescriptorSchema = {
|
||||||
|
type: required(copyValue),
|
||||||
|
id: required(convertValue),
|
||||||
|
transports: optional(copyValue)
|
||||||
|
};
|
||||||
|
var simplifiedExtensionsSchema = {
|
||||||
|
appid: optional(copyValue),
|
||||||
|
appidExclude: optional(copyValue),
|
||||||
|
credProps: optional(copyValue)
|
||||||
|
};
|
||||||
|
var simplifiedClientExtensionResultsSchema = {
|
||||||
|
appid: optional(copyValue),
|
||||||
|
appidExclude: optional(copyValue),
|
||||||
|
credProps: optional(copyValue)
|
||||||
|
};
|
||||||
|
var credentialCreationOptions = {
|
||||||
|
publicKey: required({
|
||||||
|
rp: required(copyValue),
|
||||||
|
user: required({
|
||||||
|
id: required(convertValue),
|
||||||
|
name: required(copyValue),
|
||||||
|
displayName: required(copyValue)
|
||||||
|
}),
|
||||||
|
challenge: required(convertValue),
|
||||||
|
pubKeyCredParams: required(copyValue),
|
||||||
|
timeout: optional(copyValue),
|
||||||
|
excludeCredentials: optional([publicKeyCredentialDescriptorSchema]),
|
||||||
|
authenticatorSelection: optional(copyValue),
|
||||||
|
attestation: optional(copyValue),
|
||||||
|
extensions: optional(simplifiedExtensionsSchema)
|
||||||
|
}),
|
||||||
|
signal: optional(copyValue)
|
||||||
|
};
|
||||||
|
var publicKeyCredentialWithAttestation = {
|
||||||
|
type: required(copyValue),
|
||||||
|
id: required(copyValue),
|
||||||
|
rawId: required(convertValue),
|
||||||
|
authenticatorAttachment: optional(copyValue),
|
||||||
|
response: required({
|
||||||
|
clientDataJSON: required(convertValue),
|
||||||
|
attestationObject: required(convertValue),
|
||||||
|
transports: derived(copyValue, (response) => {
|
||||||
|
var _a;
|
||||||
|
return ((_a = response.getTransports) == null ? void 0 : _a.call(response)) || [];
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
clientExtensionResults: derived(simplifiedClientExtensionResultsSchema, (pkc) => pkc.getClientExtensionResults())
|
||||||
|
};
|
||||||
|
var credentialRequestOptions = {
|
||||||
|
mediation: optional(copyValue),
|
||||||
|
publicKey: required({
|
||||||
|
challenge: required(convertValue),
|
||||||
|
timeout: optional(copyValue),
|
||||||
|
rpId: optional(copyValue),
|
||||||
|
allowCredentials: optional([publicKeyCredentialDescriptorSchema]),
|
||||||
|
userVerification: optional(copyValue),
|
||||||
|
extensions: optional(simplifiedExtensionsSchema)
|
||||||
|
}),
|
||||||
|
signal: optional(copyValue)
|
||||||
|
};
|
||||||
|
var publicKeyCredentialWithAssertion = {
|
||||||
|
type: required(copyValue),
|
||||||
|
id: required(copyValue),
|
||||||
|
rawId: required(convertValue),
|
||||||
|
authenticatorAttachment: optional(copyValue),
|
||||||
|
response: required({
|
||||||
|
clientDataJSON: required(convertValue),
|
||||||
|
authenticatorData: required(convertValue),
|
||||||
|
signature: required(convertValue),
|
||||||
|
userHandle: required(convertValue)
|
||||||
|
}),
|
||||||
|
clientExtensionResults: derived(simplifiedClientExtensionResultsSchema, (pkc) => pkc.getClientExtensionResults())
|
||||||
|
};
|
||||||
|
var schema = {
|
||||||
|
credentialCreationOptions,
|
||||||
|
publicKeyCredentialWithAttestation,
|
||||||
|
credentialRequestOptions,
|
||||||
|
publicKeyCredentialWithAssertion
|
||||||
|
};
|
||||||
|
|
||||||
|
// src/webauthn-json/basic/api.ts
|
||||||
|
function createRequestFromJSON(requestJSON) {
|
||||||
|
return convert(base64urlToBuffer, credentialCreationOptions, requestJSON);
|
||||||
|
}
|
||||||
|
function createResponseToJSON(credential) {
|
||||||
|
return convert(bufferToBase64url, publicKeyCredentialWithAttestation, credential);
|
||||||
|
}
|
||||||
|
function create(requestJSON) {
|
||||||
|
return __async(this, null, function* () {
|
||||||
|
const credential = yield navigator.credentials.create(createRequestFromJSON(requestJSON));
|
||||||
|
return createResponseToJSON(credential);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function getRequestFromJSON(requestJSON) {
|
||||||
|
return convert(base64urlToBuffer, credentialRequestOptions, requestJSON);
|
||||||
|
}
|
||||||
|
function getResponseToJSON(credential) {
|
||||||
|
return convert(bufferToBase64url, publicKeyCredentialWithAssertion, credential);
|
||||||
|
}
|
||||||
|
function get(requestJSON) {
|
||||||
|
return __async(this, null, function* () {
|
||||||
|
const credential = yield navigator.credentials.get(getRequestFromJSON(requestJSON));
|
||||||
|
return getResponseToJSON(credential);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// src/webauthn-json/basic/supported.ts
|
||||||
|
function supported() {
|
||||||
|
return !!(navigator.credentials && navigator.credentials.create && navigator.credentials.get && window.PublicKeyCredential);
|
||||||
|
}
|
||||||
|
|
||||||
|
// src/webauthn-json/browser-global.ts
|
||||||
|
globalThis.webauthnJSON = webauthn_json_exports;
|
||||||
|
})();
|
||||||
|
//# sourceMappingURL=webauthn-json.browser-global.js.map
|
|
@ -7,14 +7,12 @@
|
||||||
<form
|
<form
|
||||||
id="add-credential-form"
|
id="add-credential-form"
|
||||||
class="col-sm-6 col-sm-offset-3"
|
class="col-sm-6 col-sm-offset-3"
|
||||||
data-options="{{ options }}"
|
|
||||||
method="post"
|
method="post"
|
||||||
encrypt="multipart/form-data">
|
encrypt="multipart/form-data">
|
||||||
<h1>Add Security Key</h1>
|
<h1>Add Security Key</h1>
|
||||||
|
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input id="attestation_object" type="hidden" name="attestation_object">
|
<input id="response" type="hidden" name="response">
|
||||||
<input id="client_data_json" type="hidden" name="client_data_json">
|
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="name">Name</label>
|
<label for="name">Name</label>
|
||||||
|
@ -81,13 +79,16 @@
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{ options|json_script:"options" }}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
{% compress js %}
|
{% compress js %}
|
||||||
<script src="{% static 'js/jquery-3.6.0.min.js' %}"></script>
|
<script src="{% static 'js/jquery-3.6.0.min.js' %}"></script>
|
||||||
<script src="{% static 'js/bootstrap.min.js' %}"></script>
|
<script src="{% static 'js/bootstrap.min.js' %}"></script>
|
||||||
<script src="{% static 'js/cbor.js' %}"></script>
|
<script src="{% static 'js/webauthn-json.browser-global.js' %}"></script>
|
||||||
<script src="{% static 'js/add_credential.js' %}"></script>
|
<script src="{% static 'js/add_credential.js' %}"></script>
|
||||||
{% endcompress %}
|
{% endcompress %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
<form
|
<form
|
||||||
id="login-tfa-form"
|
id="login-tfa-form"
|
||||||
class="col-sm-6 col-sm-offset-3"
|
class="col-sm-6 col-sm-offset-3"
|
||||||
data-options="{{ options }}"
|
|
||||||
method="post"
|
method="post"
|
||||||
encrypt="multipart/form-data">
|
encrypt="multipart/form-data">
|
||||||
<h1>Two-factor Authentication</h1>
|
<h1>Two-factor Authentication</h1>
|
||||||
|
@ -34,10 +33,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input id="credential_id" type="hidden" name="credential_id">
|
<input id="response" type="hidden" name="response">
|
||||||
<input id="authenticator_data" type="hidden" name="authenticator_data">
|
|
||||||
<input id="client_data_json" type="hidden" name="client_data_json">
|
|
||||||
<input id="signature" type="hidden" name="signature">
|
|
||||||
|
|
||||||
<div id="waiting" class="hide">
|
<div id="waiting" class="hide">
|
||||||
<h2>Waiting for security key</h2>
|
<h2>Waiting for security key</h2>
|
||||||
|
@ -73,13 +69,16 @@
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{ options|json_script:"options" }}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
{% compress js %}
|
{% compress js %}
|
||||||
<script src="{% static 'js/jquery-3.6.0.min.js' %}"></script>
|
<script src="{% static 'js/jquery-3.6.0.min.js' %}"></script>
|
||||||
<script src="{% static 'js/bootstrap.min.js' %}"></script>
|
<script src="{% static 'js/bootstrap.min.js' %}"></script>
|
||||||
<script src="{% static 'js/cbor.js' %}"></script>
|
<script src="{% static 'js/webauthn-json.browser-global.js' %}"></script>
|
||||||
<script src="{% static 'js/login_tfa.js' %}"></script>
|
<script src="{% static 'js/login_tfa.js' %}"></script>
|
||||||
{% endcompress %}
|
{% endcompress %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
Loading…
Add table
Reference in a new issue