mirror of
https://github.com/healthchecks/healthchecks.git
synced 2025-04-03 04:15:29 +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 django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.contrib.auth import authenticate
|
||||
from django.contrib.auth.models import User
|
||||
from hc.accounts.models import REPORT_CHOICES, Member
|
||||
|
@ -17,17 +14,6 @@ class LowercaseEmailField(forms.EmailField):
|
|||
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):
|
||||
# Call it "identity" instead of "email"
|
||||
# to avoid some of the dumber bots
|
||||
|
@ -153,15 +139,11 @@ class TransferForm(forms.Form):
|
|||
|
||||
class AddWebAuthnForm(forms.Form):
|
||||
name = forms.CharField(max_length=100)
|
||||
client_data_json = Base64Field()
|
||||
attestation_object = Base64Field()
|
||||
response = forms.CharField()
|
||||
|
||||
|
||||
class WebAuthnForm(forms.Form):
|
||||
credential_id = Base64Field()
|
||||
client_data_json = Base64Field()
|
||||
authenticator_data = Base64Field()
|
||||
signature = Base64Field()
|
||||
response = forms.CharField()
|
||||
|
||||
|
||||
class TotpForm(forms.Form):
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import base64
|
||||
from datetime import timedelta as td
|
||||
from secrets import token_bytes, token_urlsafe
|
||||
from secrets import token_urlsafe
|
||||
from urllib.parse import urlparse
|
||||
import time
|
||||
import uuid
|
||||
|
@ -20,16 +19,12 @@ from django.utils.timezone import now
|
|||
from django.urls import resolve, reverse, Resolver404
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
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.decorators import require_sudo_mode
|
||||
from hc.accounts.models import Credential, Profile, Project, Member
|
||||
from hc.api.models import Channel, Check, TokenBucket
|
||||
from hc.payments.models import Subscription
|
||||
from hc.lib.webauthn import Server
|
||||
import pyotp
|
||||
import segno
|
||||
|
||||
|
@ -46,7 +41,7 @@ POST_LOGIN_ROUTES = (
|
|||
"hc-uncloak",
|
||||
)
|
||||
|
||||
FIDO2_SERVER = Fido2Server(PublicKeyCredentialRpEntity(settings.RP_ID, "healthchecks"))
|
||||
WEBAUTHN = Server(settings.RP_ID, "healthchecks")
|
||||
|
||||
|
||||
def _allow_redirect(redirect_url):
|
||||
|
@ -667,27 +662,6 @@ def remove_project(request, code):
|
|||
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
|
||||
@require_sudo_mode
|
||||
def add_webauthn(request):
|
||||
|
@ -699,42 +673,25 @@ def add_webauthn(request):
|
|||
if not form.is_valid():
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
credential_data = _get_credential_data(request, form)
|
||||
if not credential_data:
|
||||
try:
|
||||
auth_data = WEBAUTHN.register_complete(
|
||||
request.session["state"], form.cleaned_data["response"]
|
||||
)
|
||||
except ValueError as e:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
c = Credential(user=request.user)
|
||||
c.name = form.cleaned_data["name"]
|
||||
c.data = credential_data
|
||||
c.data = auth_data.credential_data
|
||||
c.save()
|
||||
|
||||
request.session["added_credential_name"] = c.name
|
||||
return redirect("hc-profile")
|
||||
|
||||
credentials = [c.unpack() for c in request.user.credentials.all()]
|
||||
# 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:
|
||||
options, state = FIDO2_SERVER.register_begin(
|
||||
{
|
||||
"id": token_bytes(16),
|
||||
"name": request.user.email,
|
||||
"displayName": request.user.email,
|
||||
},
|
||||
credentials,
|
||||
)
|
||||
|
||||
options, state = WEBAUTHN.register_begin(request.user.email, credentials)
|
||||
request.session["state"] = state
|
||||
|
||||
ctx = {"options": base64.b64encode(cbor.encode(options)).decode()}
|
||||
return render(request, "accounts/add_credential.html", ctx)
|
||||
return render(request, "accounts/add_credential.html", {"options": options})
|
||||
|
||||
|
||||
@login_required
|
||||
|
@ -811,30 +768,6 @@ def remove_credential(request, code):
|
|||
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):
|
||||
# We require RP_ID. Fail predicably if it is not set:
|
||||
if not settings.RP_ID:
|
||||
|
@ -863,7 +796,13 @@ def login_webauthn(request):
|
|||
if not form.is_valid():
|
||||
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()
|
||||
|
||||
request.session.pop("state")
|
||||
|
@ -871,7 +810,7 @@ def login_webauthn(request):
|
|||
auth_login(request, user, "hc.accounts.backends.EmailBackend")
|
||||
return _redirect_after_login(request)
|
||||
|
||||
options, state = FIDO2_SERVER.authenticate_begin(credentials)
|
||||
options, state = WEBAUTHN.authenticate_begin(credentials)
|
||||
request.session["state"] = state
|
||||
|
||||
totp_url = None
|
||||
|
@ -882,7 +821,7 @@ def login_webauthn(request):
|
|||
totp_url += "?next=%s" % redirect_url
|
||||
|
||||
ctx = {
|
||||
"options": base64.b64encode(cbor.encode(options)).decode(),
|
||||
"options": options,
|
||||
"totp_url": totp_url,
|
||||
}
|
||||
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() {
|
||||
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() {
|
||||
// Hide error & success messages, show the "waiting" message
|
||||
|
@ -15,10 +8,9 @@ $(function() {
|
|||
$("#error").addClass("hide");
|
||||
$("#success").addClass("hide");
|
||||
|
||||
navigator.credentials.create(options).then(function(attestation) {
|
||||
$("#attestation_object").val(b64(attestation.response.attestationObject));
|
||||
$("#client_data_json").val(b64(attestation.response.clientDataJSON));
|
||||
|
||||
var options = JSON.parse($("#options").text());
|
||||
webauthnJSON.create(options).then(function(response) {
|
||||
$("#response").val(JSON.stringify(response));
|
||||
// Show the success message and save button
|
||||
$("#waiting").addClass("hide");
|
||||
$("#success").removeClass("hide");
|
||||
|
|
|
@ -1,24 +1,14 @@
|
|||
$(function() {
|
||||
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() {
|
||||
$("#pick-method").addClass("hide");
|
||||
$("#waiting").removeClass("hide");
|
||||
$("#error").addClass("hide");
|
||||
|
||||
navigator.credentials.get(options).then(function(assertion) {
|
||||
$("#credential_id").val(b64(assertion.rawId));
|
||||
$("#authenticator_data").val(b64(assertion.response.authenticatorData));
|
||||
$("#client_data_json").val(b64(assertion.response.clientDataJSON));
|
||||
$("#signature").val(b64(assertion.response.signature));
|
||||
|
||||
var options = JSON.parse($("#options").text());
|
||||
webauthnJSON.get(options).then(function(response) {
|
||||
$("#response").val(JSON.stringify(response));
|
||||
// Show the success message and save button
|
||||
$("#waiting").addClass("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
|
||||
id="add-credential-form"
|
||||
class="col-sm-6 col-sm-offset-3"
|
||||
data-options="{{ options }}"
|
||||
method="post"
|
||||
encrypt="multipart/form-data">
|
||||
<h1>Add Security Key</h1>
|
||||
|
||||
{% csrf_token %}
|
||||
<input id="attestation_object" type="hidden" name="attestation_object">
|
||||
<input id="client_data_json" type="hidden" name="client_data_json">
|
||||
<input id="response" type="hidden" name="response">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="name">Name</label>
|
||||
|
@ -81,13 +79,16 @@
|
|||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{ options|json_script:"options" }}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{% compress js %}
|
||||
<script src="{% static 'js/jquery-3.6.0.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>
|
||||
{% endcompress %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
<form
|
||||
id="login-tfa-form"
|
||||
class="col-sm-6 col-sm-offset-3"
|
||||
data-options="{{ options }}"
|
||||
method="post"
|
||||
encrypt="multipart/form-data">
|
||||
<h1>Two-factor Authentication</h1>
|
||||
|
@ -34,10 +33,7 @@
|
|||
</div>
|
||||
|
||||
{% csrf_token %}
|
||||
<input id="credential_id" type="hidden" name="credential_id">
|
||||
<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">
|
||||
<input id="response" type="hidden" name="response">
|
||||
|
||||
<div id="waiting" class="hide">
|
||||
<h2>Waiting for security key</h2>
|
||||
|
@ -73,13 +69,16 @@
|
|||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{ options|json_script:"options" }}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{% compress js %}
|
||||
<script src="{% static 'js/jquery-3.6.0.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>
|
||||
{% endcompress %}
|
||||
{% endblock %}
|
||||
|
|
Loading…
Add table
Reference in a new issue