Compare commits
5 Commits
MoreOption
...
Conditiona
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcad5d3818 | ||
|
|
1cfddde23b | ||
|
|
98b361d73d | ||
|
|
0ab710e503 | ||
|
|
52e307ef0e |
10
CHANGELOG.md
10
CHANGELOG.md
@@ -1,4 +1,14 @@
|
||||
# Change Log
|
||||
## 3.0 (Unreleased)
|
||||
* Updated to fido2==1.1.0
|
||||
* Removed: CBOR and exchange is done in JSON now
|
||||
* Added: the following settings
|
||||
* `MFA_FIDO2_RESIDENT_KEY`: Defaults to `Discouraged` which was the old behaviour
|
||||
* `MFA_FIDO2_AUTHENTICATOR_ATTACHMENT`: If you like to have a PLATFORM Authenticator, Defaults to NONE
|
||||
* `MFA_FIDO2_USER_VERIFICATION`: If you need User Verification
|
||||
* `MFA_FIDO2_ATTESTATION_PREFERENCE`: If you like to have an Attention
|
||||
* Added: ConditionalUI support (check example)
|
||||
|
||||
## 2.6.1
|
||||
* Fix: CVE-2022-42731: related to the possibility of registration replay attack.
|
||||
Thanks to 'SSE (Secure Systems Engineering)'
|
||||
|
||||
21
README.md
21
README.md
@@ -110,7 +110,7 @@ Depends on
|
||||
* Added: `MFA_SUCCESS_REGISTRATION_MSG` & `MFA_REDIRECT_AFTER_REGISTRATION`
|
||||
* Starting version 2.6.0
|
||||
* Added: `MFA_ALWAYS_GO_TO_LAST_METHOD`, `MFA_RENAME_METHODS`, `MFA_ENFORCE_RECOVERY_METHOD` & `RECOVERY_ITERATION`
|
||||
* Starting version 2.7.0
|
||||
* Starting version 3.0
|
||||
* Added: `MFA_FIDO2_RESIDENT_KEY`, `MFA_FIDO2_AUTHENTICATOR_ATTACHMENT`, `MFA_FIDO2_USER_VERIFICATION`, `MFA_FIDO2_ATTESTATION_PREFERENCE`
|
||||
4. Break your login function
|
||||
|
||||
@@ -194,6 +194,25 @@ function some_func() {
|
||||
|
||||
````
|
||||
|
||||
# Using ConditionalUI
|
||||
|
||||

|
||||
|
||||
ConditionalUI is a cutting-edge feature that allows the user to use the browser autofill feature available in the browser to login using the FIDO2.
|
||||
|
||||
Currently available on Safari on iOS 16+, Safari on Ventura, Chrome on Android (Using Play Services Beta), Chrome Canary on Both Mac OS X and Windows.
|
||||
|
||||
To Use this feature
|
||||
1. set `MFA_FIDO2_RESIDENT_KEY` to `mfa.ResidentKey.REQUIRED`
|
||||
2. set the autocomplete of username field to `username webauth` as follows
|
||||
```html
|
||||
<input type="text" name="username" autocomplete="username webauthn" ...>
|
||||
```
|
||||
3. Finally, Include `FIDO2/FormFill.html` in your login form
|
||||
```html
|
||||
{% include 'FIDO2/FormFill.html' %}
|
||||
```
|
||||
|
||||
# Contributors
|
||||
* [mahmoodnasr](https://github.com/mahmoodnasr)
|
||||
* [d3cline](https://github.com/d3cline)
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
{% csrf_token %}
|
||||
<div class="form-group">
|
||||
<div class="form-label-group">
|
||||
<input type="text" id="inputUsername" name="username" class="form-control" placeholder="username" required="required" autofocus="autofocus">
|
||||
<input type="text" id="inputUsername" name="username" class="form-control" placeholder="username" required autofocus autocomplete="username webauthn">
|
||||
<label for="inputUsername">Username</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -46,8 +46,9 @@
|
||||
|
||||
<button class="btn btn-primary btn-block" type="submit">Login</button><br/>
|
||||
|
||||
<button class="btn btn-primary btn-block" type="button" onclick="authen()">Login By Security Key</button>
|
||||
</form>
|
||||
<button class="btn btn-primary btn-block" onclick="authen()">Login By PassKeys</button><br/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -58,7 +59,9 @@
|
||||
|
||||
<!-- Core plugin JavaScript-->
|
||||
<script src="{% static 'vendor/jquery-easing/jquery.easing.min.js'%}"></script>
|
||||
{% include 'FIDO2/Auth_JS.html'%}
|
||||
|
||||
{% include 'FIDO2/FormFill.html' %}
|
||||
{# {% include 'FIDO2/Auth_JS.html' with delay=True %}#}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
BIN
img/conditionalUI.png
Normal file
BIN
img/conditionalUI.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 111 KiB |
73
mfa/FIDO2.py
73
mfa/FIDO2.py
@@ -1,6 +1,6 @@
|
||||
from fido2.client import Fido2Client
|
||||
from fido2.server import Fido2Server, PublicKeyCredentialRpEntity
|
||||
from fido2.webauthn import AttestationObject, AuthenticatorData, CollectedClientData
|
||||
from fido2.webauthn import AttestationObject, AuthenticatorData, CollectedClientData, RegistrationResponse
|
||||
from django.template.context_processors import csrf
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.shortcuts import render
|
||||
@@ -16,8 +16,14 @@ from .views import login, reset_cookie
|
||||
import datetime
|
||||
from .Common import get_redirect_url
|
||||
from django.utils import timezone
|
||||
import fido2.features
|
||||
from django.http import JsonResponse
|
||||
|
||||
def enable_json_mapping():
|
||||
try:
|
||||
fido2.features.webauthn_json_mapping.enabled = True
|
||||
except:
|
||||
pass
|
||||
def recheck(request):
|
||||
"""Starts FIDO2 recheck"""
|
||||
context = csrf(request)
|
||||
@@ -36,6 +42,7 @@ def getServer():
|
||||
|
||||
def begin_registeration(request):
|
||||
"""Starts registering a new FIDO Device, called from API"""
|
||||
enable_json_mapping()
|
||||
server = getServer()
|
||||
from mfa import ResidentKey
|
||||
resident_key = getattr(settings,'MFA_FIDO2_RESIDENT_KEY', ResidentKey.DISCOURAGED)
|
||||
@@ -43,31 +50,30 @@ def begin_registeration(request):
|
||||
user_verification = getattr(settings,'MFA_FIDO2_USER_VERIFICATION', None)
|
||||
registration_data, state = server.register_begin({
|
||||
u'id': request.user.username.encode("utf8"),
|
||||
u'name': (request.user.first_name + " " + request.user.last_name),
|
||||
u'name': request.user.username,
|
||||
u'displayName': request.user.username,
|
||||
}, getUserCredentials(request.user.username),user_verification = user_verification,
|
||||
resident_key_requirement = resident_key, authenticator_attachment = auth_attachment)
|
||||
request.session['fido_state'] = state
|
||||
|
||||
return HttpResponse(cbor.encode(registration_data), content_type = 'application/octet-stream')
|
||||
request.session['fido2_state'] = state
|
||||
return JsonResponse(dict(registration_data))
|
||||
#return HttpResponse(cbor.encode(registration_data), content_type = 'application/octet-stream')
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
def complete_reg(request):
|
||||
"""Completes the registeration, called by API"""
|
||||
try:
|
||||
if not "fido_state" in request.session:
|
||||
if not "fido2_state" in request.session:
|
||||
return JsonResponse({'status': 'ERR', "message": "FIDO Status can't be found, please try again"})
|
||||
data = cbor.decode(request.body)
|
||||
|
||||
client_data = CollectedClientData(data['clientDataJSON'])
|
||||
att_obj = AttestationObject((data['attestationObject']))
|
||||
enable_json_mapping()
|
||||
data = simplejson.loads(request.body)
|
||||
server = getServer()
|
||||
auth_data = server.register_complete(
|
||||
request.session.pop('fido_state'),
|
||||
client_data,
|
||||
att_obj
|
||||
)
|
||||
auth_data = server.register_complete(request.session["fido2_state"], response = data)
|
||||
registration = RegistrationResponse.from_dict(data)
|
||||
attestation_object = registration.response.attestation_object
|
||||
#auth_data = attestation_object.auth_data
|
||||
att_obj = attestation_object
|
||||
|
||||
encoded = websafe_encode(auth_data.credential_data)
|
||||
uk = User_Keys()
|
||||
uk.username = request.user.username
|
||||
@@ -103,10 +109,7 @@ def start(request):
|
||||
|
||||
|
||||
def getUserCredentials(username):
|
||||
credentials = []
|
||||
for uk in User_Keys.objects.filter(username = username, key_type = "FIDO2"):
|
||||
credentials.append(AttestedCredentialData(websafe_decode(uk.properties["device"])))
|
||||
return credentials
|
||||
return [AttestedCredentialData(websafe_decode(uk.properties["device"])) for uk in User_Keys.objects.filter(username = username, key_type = "FIDO2")]
|
||||
|
||||
|
||||
def auth(request):
|
||||
@@ -115,6 +118,7 @@ def auth(request):
|
||||
|
||||
|
||||
def authenticate_begin(request):
|
||||
enable_json_mapping()
|
||||
server = getServer()
|
||||
credentials=[]
|
||||
username = None
|
||||
@@ -125,13 +129,14 @@ def authenticate_begin(request):
|
||||
if username:
|
||||
credentials = getUserCredentials(request.session.get("base_username", request.user.username))
|
||||
auth_data, state = server.authenticate_begin(credentials)
|
||||
request.session['fido_state'] = state
|
||||
return HttpResponse(cbor.encode(auth_data), content_type = "application/octet-stream")
|
||||
request.session['fido2_state'] = state
|
||||
return JsonResponse(dict(auth_data))
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
def authenticate_complete(request):
|
||||
try:
|
||||
enable_json_mapping()
|
||||
credentials = []
|
||||
username = None
|
||||
keys = None
|
||||
@@ -140,26 +145,28 @@ def authenticate_complete(request):
|
||||
if request.user.is_authenticated:
|
||||
username = request.user.username
|
||||
server = getServer()
|
||||
data = cbor.decode(request.body)
|
||||
credential_id = data['credentialId']
|
||||
if credential_id and username is None:
|
||||
data = simplejson.loads(request.body)
|
||||
userHandle = data.get("response",{}).get('userHandle')
|
||||
credential_id = data['id']
|
||||
|
||||
if userHandle:
|
||||
if User_Keys.objects.filter(username=userHandle).exists():
|
||||
credentials = getUserCredentials(userHandle)
|
||||
username=userHandle
|
||||
else:
|
||||
keys = User_Keys.objects.filter(user_handle = userHandle)
|
||||
if keys.exists():
|
||||
credentials = [AttestedCredentialData(websafe_decode(keys[0].properties["device"]))]
|
||||
elif credential_id and username is None:
|
||||
keys = User_Keys.objects.filter(user_handle = credential_id)
|
||||
if keys.exists():
|
||||
credentials=[AttestedCredentialData(websafe_decode(keys[0].properties["device"]))]
|
||||
else:
|
||||
credentials = getUserCredentials(username)
|
||||
|
||||
client_data = CollectedClientData(data['clientDataJSON'])
|
||||
auth_data = AuthenticatorData(data['authenticatorData'])
|
||||
signature = data['signature']
|
||||
try:
|
||||
cred = server.authenticate_complete(
|
||||
request.session.pop('fido_state'),
|
||||
credentials,
|
||||
credential_id,
|
||||
client_data,
|
||||
auth_data,
|
||||
signature
|
||||
request.session.pop('fido2_state'), credentials = credentials, response = data
|
||||
)
|
||||
except ValueError:
|
||||
return HttpResponse(simplejson.dumps({'status': "ERR",
|
||||
|
||||
73
mfa/static/mfa/js/base64url.js
Normal file
73
mfa/static/mfa/js/base64url.js
Normal file
@@ -0,0 +1,73 @@
|
||||
(function(){
|
||||
'use strict';
|
||||
|
||||
let chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
||||
|
||||
// Use a lookup table to find the index.
|
||||
let lookup = new Uint8Array(256);
|
||||
for (let i = 0; i < chars.length; i++) {
|
||||
lookup[chars.charCodeAt(i)] = i;
|
||||
}
|
||||
|
||||
let encode = function(arraybuffer) {
|
||||
let bytes = new Uint8Array(arraybuffer),
|
||||
i, len = bytes.length, base64url = '';
|
||||
|
||||
for (i = 0; i < len; i+=3) {
|
||||
base64url += chars[bytes[i] >> 2];
|
||||
base64url += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)];
|
||||
base64url += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)];
|
||||
base64url += chars[bytes[i + 2] & 63];
|
||||
}
|
||||
|
||||
if ((len % 3) === 2) {
|
||||
base64url = base64url.substring(0, base64url.length - 1);
|
||||
} else if (len % 3 === 1) {
|
||||
base64url = base64url.substring(0, base64url.length - 2);
|
||||
}
|
||||
|
||||
return base64url;
|
||||
};
|
||||
|
||||
let decode = function(base64string) {
|
||||
let bufferLength = base64string.length * 0.75,
|
||||
len = base64string.length, i, p = 0,
|
||||
encoded1, encoded2, encoded3, encoded4;
|
||||
|
||||
let bytes = new Uint8Array(bufferLength);
|
||||
|
||||
for (i = 0; i < len; i+=4) {
|
||||
encoded1 = lookup[base64string.charCodeAt(i)];
|
||||
encoded2 = lookup[base64string.charCodeAt(i+1)];
|
||||
encoded3 = lookup[base64string.charCodeAt(i+2)];
|
||||
encoded4 = lookup[base64string.charCodeAt(i+3)];
|
||||
|
||||
bytes[p++] = (encoded1 << 2) | (encoded2 >> 4);
|
||||
bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2);
|
||||
bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63);
|
||||
}
|
||||
|
||||
return bytes.buffer
|
||||
};
|
||||
|
||||
let methods = {
|
||||
'decode': decode,
|
||||
'encode': encode
|
||||
}
|
||||
|
||||
/**
|
||||
* Exporting and stuff
|
||||
*/
|
||||
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
|
||||
module.exports = methods;
|
||||
|
||||
} else {
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
define([], function() {
|
||||
return methods
|
||||
});
|
||||
} else {
|
||||
window.base64url = methods;
|
||||
}
|
||||
}
|
||||
})();
|
||||
@@ -1,406 +0,0 @@
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2016 Patrick Gansterer <paroga@paroga.com>
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
(function(global, undefined) { "use strict";
|
||||
var POW_2_24 = 5.960464477539063e-8,
|
||||
POW_2_32 = 4294967296,
|
||||
POW_2_53 = 9007199254740992;
|
||||
|
||||
function encode(value) {
|
||||
var data = new ArrayBuffer(256);
|
||||
var dataView = new DataView(data);
|
||||
var lastLength;
|
||||
var offset = 0;
|
||||
|
||||
function prepareWrite(length) {
|
||||
var newByteLength = data.byteLength;
|
||||
var requiredLength = offset + length;
|
||||
while (newByteLength < requiredLength)
|
||||
newByteLength <<= 1;
|
||||
if (newByteLength !== data.byteLength) {
|
||||
var oldDataView = dataView;
|
||||
data = new ArrayBuffer(newByteLength);
|
||||
dataView = new DataView(data);
|
||||
var uint32count = (offset + 3) >> 2;
|
||||
for (var i = 0; i < uint32count; ++i)
|
||||
dataView.setUint32(i << 2, oldDataView.getUint32(i << 2));
|
||||
}
|
||||
|
||||
lastLength = length;
|
||||
return dataView;
|
||||
}
|
||||
function commitWrite() {
|
||||
offset += lastLength;
|
||||
}
|
||||
function writeFloat64(value) {
|
||||
commitWrite(prepareWrite(8).setFloat64(offset, value));
|
||||
}
|
||||
function writeUint8(value) {
|
||||
commitWrite(prepareWrite(1).setUint8(offset, value));
|
||||
}
|
||||
function writeUint8Array(value) {
|
||||
var dataView = prepareWrite(value.length);
|
||||
for (var i = 0; i < value.length; ++i)
|
||||
dataView.setUint8(offset + i, value[i]);
|
||||
commitWrite();
|
||||
}
|
||||
function writeUint16(value) {
|
||||
commitWrite(prepareWrite(2).setUint16(offset, value));
|
||||
}
|
||||
function writeUint32(value) {
|
||||
commitWrite(prepareWrite(4).setUint32(offset, value));
|
||||
}
|
||||
function writeUint64(value) {
|
||||
var low = value % POW_2_32;
|
||||
var high = (value - low) / POW_2_32;
|
||||
var dataView = prepareWrite(8);
|
||||
dataView.setUint32(offset, high);
|
||||
dataView.setUint32(offset + 4, low);
|
||||
commitWrite();
|
||||
}
|
||||
function writeTypeAndLength(type, length) {
|
||||
if (length < 24) {
|
||||
writeUint8(type << 5 | length);
|
||||
} else if (length < 0x100) {
|
||||
writeUint8(type << 5 | 24);
|
||||
writeUint8(length);
|
||||
} else if (length < 0x10000) {
|
||||
writeUint8(type << 5 | 25);
|
||||
writeUint16(length);
|
||||
} else if (length < 0x100000000) {
|
||||
writeUint8(type << 5 | 26);
|
||||
writeUint32(length);
|
||||
} else {
|
||||
writeUint8(type << 5 | 27);
|
||||
writeUint64(length);
|
||||
}
|
||||
}
|
||||
|
||||
function encodeItem(value) {
|
||||
var i;
|
||||
|
||||
if (value === false)
|
||||
return writeUint8(0xf4);
|
||||
if (value === true)
|
||||
return writeUint8(0xf5);
|
||||
if (value === null)
|
||||
return writeUint8(0xf6);
|
||||
if (value === undefined)
|
||||
return writeUint8(0xf7);
|
||||
|
||||
switch (typeof value) {
|
||||
case "number":
|
||||
if (Math.floor(value) === value) {
|
||||
if (0 <= value && value <= POW_2_53)
|
||||
return writeTypeAndLength(0, value);
|
||||
if (-POW_2_53 <= value && value < 0)
|
||||
return writeTypeAndLength(1, -(value + 1));
|
||||
}
|
||||
writeUint8(0xfb);
|
||||
return writeFloat64(value);
|
||||
|
||||
case "string":
|
||||
var utf8data = [];
|
||||
for (i = 0; i < value.length; ++i) {
|
||||
var charCode = value.charCodeAt(i);
|
||||
if (charCode < 0x80) {
|
||||
utf8data.push(charCode);
|
||||
} else if (charCode < 0x800) {
|
||||
utf8data.push(0xc0 | charCode >> 6);
|
||||
utf8data.push(0x80 | charCode & 0x3f);
|
||||
} else if (charCode < 0xd800) {
|
||||
utf8data.push(0xe0 | charCode >> 12);
|
||||
utf8data.push(0x80 | (charCode >> 6) & 0x3f);
|
||||
utf8data.push(0x80 | charCode & 0x3f);
|
||||
} else {
|
||||
charCode = (charCode & 0x3ff) << 10;
|
||||
charCode |= value.charCodeAt(++i) & 0x3ff;
|
||||
charCode += 0x10000;
|
||||
|
||||
utf8data.push(0xf0 | charCode >> 18);
|
||||
utf8data.push(0x80 | (charCode >> 12) & 0x3f);
|
||||
utf8data.push(0x80 | (charCode >> 6) & 0x3f);
|
||||
utf8data.push(0x80 | charCode & 0x3f);
|
||||
}
|
||||
}
|
||||
|
||||
writeTypeAndLength(3, utf8data.length);
|
||||
return writeUint8Array(utf8data);
|
||||
|
||||
default:
|
||||
var length;
|
||||
if (Array.isArray(value)) {
|
||||
length = value.length;
|
||||
writeTypeAndLength(4, length);
|
||||
for (i = 0; i < length; ++i)
|
||||
encodeItem(value[i]);
|
||||
} else if (value instanceof Uint8Array) {
|
||||
writeTypeAndLength(2, value.length);
|
||||
writeUint8Array(value);
|
||||
} else {
|
||||
var keys = Object.keys(value);
|
||||
length = keys.length;
|
||||
writeTypeAndLength(5, length);
|
||||
for (i = 0; i < length; ++i) {
|
||||
var key = keys[i];
|
||||
encodeItem(key);
|
||||
encodeItem(value[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
encodeItem(value);
|
||||
|
||||
if ("slice" in data)
|
||||
return data.slice(0, offset);
|
||||
|
||||
var ret = new ArrayBuffer(offset);
|
||||
var retView = new DataView(ret);
|
||||
for (var i = 0; i < offset; ++i)
|
||||
retView.setUint8(i, dataView.getUint8(i));
|
||||
return ret;
|
||||
}
|
||||
|
||||
function decode(data, tagger, simpleValue) {
|
||||
var dataView = new DataView(data);
|
||||
var offset = 0;
|
||||
|
||||
if (typeof tagger !== "function")
|
||||
tagger = function(value) { return value; };
|
||||
if (typeof simpleValue !== "function")
|
||||
simpleValue = function() { return undefined; };
|
||||
|
||||
function commitRead(length, value) {
|
||||
offset += length;
|
||||
return value;
|
||||
}
|
||||
function readArrayBuffer(length) {
|
||||
return commitRead(length, new Uint8Array(data, offset, length));
|
||||
}
|
||||
function readFloat16() {
|
||||
var tempArrayBuffer = new ArrayBuffer(4);
|
||||
var tempDataView = new DataView(tempArrayBuffer);
|
||||
var value = readUint16();
|
||||
|
||||
var sign = value & 0x8000;
|
||||
var exponent = value & 0x7c00;
|
||||
var fraction = value & 0x03ff;
|
||||
|
||||
if (exponent === 0x7c00)
|
||||
exponent = 0xff << 10;
|
||||
else if (exponent !== 0)
|
||||
exponent += (127 - 15) << 10;
|
||||
else if (fraction !== 0)
|
||||
return (sign ? -1 : 1) * fraction * POW_2_24;
|
||||
|
||||
tempDataView.setUint32(0, sign << 16 | exponent << 13 | fraction << 13);
|
||||
return tempDataView.getFloat32(0);
|
||||
}
|
||||
function readFloat32() {
|
||||
return commitRead(4, dataView.getFloat32(offset));
|
||||
}
|
||||
function readFloat64() {
|
||||
return commitRead(8, dataView.getFloat64(offset));
|
||||
}
|
||||
function readUint8() {
|
||||
return commitRead(1, dataView.getUint8(offset));
|
||||
}
|
||||
function readUint16() {
|
||||
return commitRead(2, dataView.getUint16(offset));
|
||||
}
|
||||
function readUint32() {
|
||||
return commitRead(4, dataView.getUint32(offset));
|
||||
}
|
||||
function readUint64() {
|
||||
return readUint32() * POW_2_32 + readUint32();
|
||||
}
|
||||
function readBreak() {
|
||||
if (dataView.getUint8(offset) !== 0xff)
|
||||
return false;
|
||||
offset += 1;
|
||||
return true;
|
||||
}
|
||||
function readLength(additionalInformation) {
|
||||
if (additionalInformation < 24)
|
||||
return additionalInformation;
|
||||
if (additionalInformation === 24)
|
||||
return readUint8();
|
||||
if (additionalInformation === 25)
|
||||
return readUint16();
|
||||
if (additionalInformation === 26)
|
||||
return readUint32();
|
||||
if (additionalInformation === 27)
|
||||
return readUint64();
|
||||
if (additionalInformation === 31)
|
||||
return -1;
|
||||
throw "Invalid length encoding";
|
||||
}
|
||||
function readIndefiniteStringLength(majorType) {
|
||||
var initialByte = readUint8();
|
||||
if (initialByte === 0xff)
|
||||
return -1;
|
||||
var length = readLength(initialByte & 0x1f);
|
||||
if (length < 0 || (initialByte >> 5) !== majorType)
|
||||
throw "Invalid indefinite length element";
|
||||
return length;
|
||||
}
|
||||
|
||||
function appendUtf16Data(utf16data, length) {
|
||||
for (var i = 0; i < length; ++i) {
|
||||
var value = readUint8();
|
||||
if (value & 0x80) {
|
||||
if (value < 0xe0) {
|
||||
value = (value & 0x1f) << 6
|
||||
| (readUint8() & 0x3f);
|
||||
length -= 1;
|
||||
} else if (value < 0xf0) {
|
||||
value = (value & 0x0f) << 12
|
||||
| (readUint8() & 0x3f) << 6
|
||||
| (readUint8() & 0x3f);
|
||||
length -= 2;
|
||||
} else {
|
||||
value = (value & 0x0f) << 18
|
||||
| (readUint8() & 0x3f) << 12
|
||||
| (readUint8() & 0x3f) << 6
|
||||
| (readUint8() & 0x3f);
|
||||
length -= 3;
|
||||
}
|
||||
}
|
||||
|
||||
if (value < 0x10000) {
|
||||
utf16data.push(value);
|
||||
} else {
|
||||
value -= 0x10000;
|
||||
utf16data.push(0xd800 | (value >> 10));
|
||||
utf16data.push(0xdc00 | (value & 0x3ff));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function decodeItem() {
|
||||
var initialByte = readUint8();
|
||||
var majorType = initialByte >> 5;
|
||||
var additionalInformation = initialByte & 0x1f;
|
||||
var i;
|
||||
var length;
|
||||
|
||||
if (majorType === 7) {
|
||||
switch (additionalInformation) {
|
||||
case 25:
|
||||
return readFloat16();
|
||||
case 26:
|
||||
return readFloat32();
|
||||
case 27:
|
||||
return readFloat64();
|
||||
}
|
||||
}
|
||||
|
||||
length = readLength(additionalInformation);
|
||||
if (length < 0 && (majorType < 2 || 6 < majorType))
|
||||
throw "Invalid length";
|
||||
|
||||
switch (majorType) {
|
||||
case 0:
|
||||
return length;
|
||||
case 1:
|
||||
return -1 - length;
|
||||
case 2:
|
||||
if (length < 0) {
|
||||
var elements = [];
|
||||
var fullArrayLength = 0;
|
||||
while ((length = readIndefiniteStringLength(majorType)) >= 0) {
|
||||
fullArrayLength += length;
|
||||
elements.push(readArrayBuffer(length));
|
||||
}
|
||||
var fullArray = new Uint8Array(fullArrayLength);
|
||||
var fullArrayOffset = 0;
|
||||
for (i = 0; i < elements.length; ++i) {
|
||||
fullArray.set(elements[i], fullArrayOffset);
|
||||
fullArrayOffset += elements[i].length;
|
||||
}
|
||||
return fullArray;
|
||||
}
|
||||
return readArrayBuffer(length);
|
||||
case 3:
|
||||
var utf16data = [];
|
||||
if (length < 0) {
|
||||
while ((length = readIndefiniteStringLength(majorType)) >= 0)
|
||||
appendUtf16Data(utf16data, length);
|
||||
} else
|
||||
appendUtf16Data(utf16data, length);
|
||||
return String.fromCharCode.apply(null, utf16data);
|
||||
case 4:
|
||||
var retArray;
|
||||
if (length < 0) {
|
||||
retArray = [];
|
||||
while (!readBreak())
|
||||
retArray.push(decodeItem());
|
||||
} else {
|
||||
retArray = new Array(length);
|
||||
for (i = 0; i < length; ++i)
|
||||
retArray[i] = decodeItem();
|
||||
}
|
||||
return retArray;
|
||||
case 5:
|
||||
var retObject = {};
|
||||
for (i = 0; i < length || length < 0 && !readBreak(); ++i) {
|
||||
var key = decodeItem();
|
||||
retObject[key] = decodeItem();
|
||||
}
|
||||
return retObject;
|
||||
case 6:
|
||||
return tagger(decodeItem(), length);
|
||||
case 7:
|
||||
switch (length) {
|
||||
case 20:
|
||||
return false;
|
||||
case 21:
|
||||
return true;
|
||||
case 22:
|
||||
return null;
|
||||
case 23:
|
||||
return undefined;
|
||||
default:
|
||||
return simpleValue(length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var ret = decodeItem();
|
||||
if (offset !== data.byteLength)
|
||||
throw "Remaining bytes";
|
||||
return ret;
|
||||
}
|
||||
|
||||
var obj = { encode: encode, decode: decode };
|
||||
|
||||
if (typeof define === "function" && define.amd)
|
||||
define("cbor/cbor", obj);
|
||||
else if (typeof module !== "undefined" && module.exports)
|
||||
module.exports = obj;
|
||||
else if (!global.CBOR)
|
||||
global.CBOR = obj;
|
||||
|
||||
})(this);
|
||||
25
mfa/static/mfa/js/helpers.js
Normal file
25
mfa/static/mfa/js/helpers.js
Normal file
@@ -0,0 +1,25 @@
|
||||
var publicKeyCredentialToJSON = (pubKeyCred) => {
|
||||
if(pubKeyCred instanceof Array) {
|
||||
let arr = [];
|
||||
for(let i of pubKeyCred)
|
||||
arr.push(publicKeyCredentialToJSON(i));
|
||||
|
||||
return arr
|
||||
}
|
||||
|
||||
if(pubKeyCred instanceof ArrayBuffer) {
|
||||
return base64url.encode(pubKeyCred)
|
||||
}
|
||||
|
||||
if(pubKeyCred instanceof Object) {
|
||||
let obj = {};
|
||||
|
||||
for (let key in pubKeyCred) {
|
||||
obj[key] = publicKeyCredentialToJSON(pubKeyCred[key])
|
||||
}
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
return pubKeyCred
|
||||
}
|
||||
@@ -1,17 +1,31 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% block head %}
|
||||
<script type="application/javascript" src="{% static 'mfa/js/cbor.js'%}"></script>
|
||||
<script type="application/javascript" src="{% static 'mfa/js/ua-parser.min.js'%}"></script>
|
||||
<script type="application/javascript" src="{% static 'mfa/js/base64url.js'%}"></script>
|
||||
<script type="application/javascript" src="{% static 'mfa/js/helpers.js'%}"></script>
|
||||
<script type="application/javascript">
|
||||
var MakeCredReq = (makeCredReq) => {
|
||||
makeCredReq.publicKey.challenge = base64url.decode(makeCredReq.publicKey.challenge);
|
||||
makeCredReq.publicKey.user.id = base64url.decode(makeCredReq.publicKey.user.id);
|
||||
|
||||
for(let excludeCred of makeCredReq.publicKey.excludeCredentials) {
|
||||
excludeCred.id = base64url.decode(excludeCred.id);
|
||||
}
|
||||
|
||||
return makeCredReq
|
||||
}
|
||||
function begin_reg(){
|
||||
fetch('{% url 'fido2_begin_reg' %}',{}).then(function(response) {
|
||||
if(response.ok)
|
||||
{
|
||||
return response.arrayBuffer();
|
||||
return response.json().then(function (req){
|
||||
return MakeCredReq(req)
|
||||
});
|
||||
}
|
||||
throw new Error('Error getting registration data!');
|
||||
}).then(CBOR.decode).then(function(options) {
|
||||
}).then(function(options) {
|
||||
|
||||
//options.publicKey.attestation="direct"
|
||||
console.log(options)
|
||||
|
||||
@@ -19,11 +33,7 @@
|
||||
}).then(function(attestation) {
|
||||
return fetch('{% url 'fido2_complete_reg' %}', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/cbor'},
|
||||
body: CBOR.encode({
|
||||
"attestationObject": new Uint8Array(attestation.response.attestationObject),
|
||||
"clientDataJSON": new Uint8Array(attestation.response.clientDataJSON),
|
||||
})
|
||||
body: JSON.stringify(publicKeyCredentialToJSON(attestation))
|
||||
});
|
||||
}).then(function(response) {
|
||||
|
||||
|
||||
@@ -1,30 +1,46 @@
|
||||
{% load static %}
|
||||
<script type="application/javascript" src="{% static 'mfa/js/cbor.js' %}"></script>
|
||||
<script type="application/javascript" src="{% static 'mfa/js/base64url.js' %}"></script>
|
||||
<script type="application/javascript" src="{% static 'mfa/js/helpers.js' %}"></script>
|
||||
<script type="application/javascript" src="{% static 'mfa/js/ua-parser.min.js' %}"></script>
|
||||
<script type="text/javascript">
|
||||
function authen()
|
||||
var GetAssertReq = (getAssert) => {
|
||||
getAssert.publicKey.challenge = base64url.decode(getAssert.publicKey.challenge);
|
||||
|
||||
for(let allowCred of getAssert.publicKey.allowCredentials) {
|
||||
allowCred.id = base64url.decode(allowCred.id);
|
||||
}
|
||||
|
||||
return getAssert
|
||||
}
|
||||
function authen(conditionalUI=false)
|
||||
{
|
||||
fetch('{% url 'fido2_begin_auth' %}', {
|
||||
if (window.hasOwnProperty("getCredSignal"))
|
||||
window.getCredSignal.abort('Restarting');
|
||||
window.getCredSignal = new AbortController();
|
||||
fetch('{% url 'fido2_begin_auth' %}', {
|
||||
method: 'GET',
|
||||
}).then(function(response) {
|
||||
if(response.ok) return response.arrayBuffer();
|
||||
if(response.ok) {
|
||||
return response.json().then(function (req){
|
||||
return GetAssertReq(req)
|
||||
});
|
||||
}
|
||||
throw new Error('No credential available to authenticate!');
|
||||
}).then(CBOR.decode).then(function(options) {
|
||||
}).then(function(options) {
|
||||
console.log(options)
|
||||
return navigator.credentials.get(options);
|
||||
if (conditionalUI) {
|
||||
options.mediation = 'conditional';
|
||||
}
|
||||
options.signal = window.getCredSignal.signal;
|
||||
return navigator.credentials.get(options);
|
||||
}).then(function(assertion) {
|
||||
res=CBOR.encode({
|
||||
"credentialId": new Uint8Array(assertion.rawId),
|
||||
"authenticatorData": new Uint8Array(assertion.response.authenticatorData),
|
||||
"clientDataJSON": new Uint8Array(assertion.response.clientDataJSON),
|
||||
"signature": new Uint8Array(assertion.response.signature)
|
||||
});
|
||||
|
||||
|
||||
return fetch('{% url 'fido2_complete_auth' %}', {
|
||||
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/cbor'},
|
||||
body:res,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body:JSON.stringify(publicKeyCredentialToJSON(assertion)),
|
||||
|
||||
}).then(function (response) {if (response.ok) return res = response.json()}).then(function (res) {
|
||||
if (res.status=="OK")
|
||||
@@ -61,8 +77,11 @@
|
||||
ua=new UAParser().getResult()
|
||||
if (ua.browser.name == "Safari" || ua.browser.name == "Mobile Safari" || ua.os.name == "iOS" || ua.os.name == "iPadOS")
|
||||
$("#res").html("<button class='btn btn-success' onclick='authen()'>Authenticate...</button>")
|
||||
else
|
||||
authen()
|
||||
else {
|
||||
{% if delay != True and not conditionalUI%}
|
||||
authen()
|
||||
{% endif %}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
15
mfa/templates/FIDO2/FormFill.html
Normal file
15
mfa/templates/FIDO2/FormFill.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<script type="application/javascript">
|
||||
$(document).ready(async function () {
|
||||
if (window.PublicKeyCredential &&
|
||||
PublicKeyCredential.isConditionalMediationAvailable
|
||||
)
|
||||
{
|
||||
// Check if conditional mediation is available.
|
||||
const isCMA = await PublicKeyCredential.isConditionalMediationAvailable()
|
||||
if (isCMA) {
|
||||
authen(true);
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% include 'FIDO2/Auth_JS.html' %}
|
||||
@@ -6,5 +6,5 @@ python-u2flib-server
|
||||
ua-parser
|
||||
user-agents
|
||||
python-jose
|
||||
fido2 == 1.0.0
|
||||
fido2 == 1.1.0
|
||||
jsonLookup
|
||||
|
||||
4
setup.py
4
setup.py
@@ -4,7 +4,7 @@ from setuptools import find_packages, setup
|
||||
|
||||
setup(
|
||||
name='django-mfa2',
|
||||
version='2.6.1',
|
||||
version='3.0',
|
||||
description='Allows user to add 2FA to their accounts',
|
||||
long_description=open("README.md").read(),
|
||||
long_description_content_type="text/markdown",
|
||||
@@ -24,7 +24,7 @@ setup(
|
||||
'ua-parser',
|
||||
'user-agents',
|
||||
'python-jose',
|
||||
'fido2 == 1.0.0',
|
||||
'fido2 == 1.1.0',
|
||||
'jsonLookup'
|
||||
],
|
||||
python_requires=">=3.5",
|
||||
|
||||
Reference in New Issue
Block a user