Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fido2: use JSON encoding #13

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,9 @@ jobs:
matrix:
include:
- python: '3.9'
django: '3.2'
- python: '3.12'
django: '4.2'
- python: '3.12'
django: '5.1'
- python: '3.13'
django: '5.2a1'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ pip install django-mfa3
`settings.py` for a full list of settings.
4. Register URLs: `path('mfa/', include('mfa.urls', namespace='mfa')`
5. The included templates are just examples, so you should [replace them](https://docs.djangoproject.com/en/stable/howto/overriding-templates/) with your own
6. FIDO2 requires client side code. You can either implement it yourself or use the included fido2.js (in which case you will have to provide the third party library [cbor-js](https://www.npmjs.com/package/cbor-js)).
6. FIDO2 requires client side code. You can either implement it yourself or use the included fido2.js (in which case you will have to provide the third party library [@github/webauthn-json](https://www.npmjs.com/package/@github/webauthn-json)).
7. Somewhere in your app, add a link to `'mfa:list'`

## Enforce MFA
Expand Down
44 changes: 13 additions & 31 deletions mfa/methods/fido2.py
Original file line number Diff line number Diff line change
@@ -1,57 +1,43 @@
from fido2 import cbor
import json

from fido2.features import webauthn_json_mapping
from fido2.server import Fido2Server
from fido2.utils import websafe_decode
from fido2.utils import websafe_encode
from fido2.webauthn import AttestationObject
from fido2.webauthn import AttestedCredentialData
from fido2.webauthn import AuthenticatorData
from fido2.webauthn import CollectedClientData
from fido2.webauthn import PublicKeyCredentialRpEntity
from fido2.webauthn import PublicKeyCredentialUserEntity

from .. import settings

name = 'FIDO2'

webauthn_json_mapping.enabled = False
webauthn_json_mapping.enabled = True
fido2 = Fido2Server(
PublicKeyCredentialRpEntity(id=settings.DOMAIN, name=settings.SITE_TITLE),
)


def encode(data):
return cbor.encode(data).hex()


def decode(s):
return cbor.decode(bytes.fromhex(s))


def get_credentials(user):
keys = user.mfakey_set.filter(method=name)
return [AttestedCredentialData(websafe_decode(key.secret)) for key in keys]


def register_begin(user):
registration_data, state = fido2.register_begin(
{
'id': str(user.id).encode('utf-8'),
'name': user.get_username(),
'displayName': user.get_full_name(),
},
PublicKeyCredentialUserEntity(
id=str(user.id).encode('utf-8'),
name=user.get_username(),
display_name=user.get_full_name(),
),
get_credentials(user),
user_verification=settings.FIDO2_USER_VERIFICATION,
)
return encode(registration_data), state
return json.dumps(dict(registration_data)), state


def register_complete(state, request_data):
data = decode(request_data)
auth_data = fido2.register_complete(
state,
CollectedClientData(data['clientData']),
AttestationObject(data['attestationObject']),
)
auth_data = fido2.register_complete(state, json.loads(request_data))
return websafe_encode(auth_data.credential_data)


Expand All @@ -61,16 +47,12 @@ def authenticate_begin(user):
credentials,
user_verification=settings.FIDO2_USER_VERIFICATION,
)
return encode(auth_data), state
return json.dumps(dict(auth_data)), state


def authenticate_complete(state, user, request_data):
data = decode(request_data)
fido2.authenticate_complete(
state,
get_credentials(user),
data['credentialId'],
CollectedClientData(data['clientData']),
AuthenticatorData(data['authenticatorData']),
data['signature'],
json.loads(request_data),
)
84 changes: 33 additions & 51 deletions mfa/static/mfa/fido2.js
Original file line number Diff line number Diff line change
@@ -1,56 +1,38 @@
(function() {
var encode = function(data) {
var buffer = CBOR.encode(data);
var arr = new Uint8Array(buffer);
return arr.reduce((s, b) => s + b.toString(16).padStart(2, '0'), '');
};
import * as webauthnJSON from 'webauthn-json';

var decode = function(hex) {
var arr = new Uint8Array(hex.length / 2);
for (var i = 0; i < arr.length; i += 1) {
arr[i] = parseInt(hex.substring(i * 2, i * 2 + 2), 16);
}
return CBOR.decode(arr.buffer);
};
var initCreate = function() {
var form = document.querySelector('form[data-fido2-create]');
if (form) {
var options = webauthnJSON.parseCreationOptionsFromJSON(
JSON.parse(form.dataset.fido2Create)
);
form.addEventListener('submit', function(event) {
event.preventDefault();

var initCreate = function() {
var form = document.querySelector('form[data-fido2-create]');
if (form) {
var options = decode(form.dataset.fido2Create);
form.addEventListener('submit', function(event) {
event.preventDefault();
webauthnJSON.create(options).then(attestation => {
this.code.value = JSON.stringify(attestation);
form.submit();
}).catch(alert);
});
}
};

navigator.credentials.create(options).then(attestation => {
this.code.value = encode({
'attestationObject': new Uint8Array(attestation.response.attestationObject),
'clientData': new Uint8Array(attestation.response.clientDataJSON),
});
form.submit();
}).catch(alert);
});
}
};
var initAuth = function() {
var form = document.querySelector('form[data-fido2-auth]');
if (form) {
var options = webauthnJSON.parseRequestOptionsFromJSON(
JSON.parse(form.dataset.fido2Auth)
);
form.addEventListener('submit', function(event) {
event.preventDefault();

var initAuth = function() {
var form = document.querySelector('form[data-fido2-auth]');
if (form) {
var options = decode(form.dataset.fido2Auth);
form.addEventListener('submit', function(event) {
event.preventDefault();
webauthnJSON.get(options).then(assertion => {
this.code.value = JSON.stringify(assertion);
form.submit();
}).catch(alert);
});
}
};

navigator.credentials.get(options).then(assertion => {
this.code.value = encode({
'credentialId': new Uint8Array(assertion.rawId),
'authenticatorData': new Uint8Array(assertion.response.authenticatorData),
'clientData': new Uint8Array(assertion.response.clientDataJSON),
'signature': new Uint8Array(assertion.response.signature),
});
form.submit();
}).catch(alert);
});
}
};

initCreate();
initAuth();
})();
initCreate();
initAuth();
4 changes: 2 additions & 2 deletions mfa/templates/mfa/auth_FIDO2.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ <h1>Two-factor authentication</h1>
<a href="{% url 'mfa:auth' 'recovery' %}">Use recovery code instead</a>
</form>

<script src="{% static 'cbor-js/cbor.js' %}"></script>
<script src="{% static 'mfa/fido2.js' %}"></script>
<script type="importmap">{"imports": {"webauthn-json": "https://cdn.jsdelivr.net/npm/@github/[email protected]/dist/esm/webauthn-json.browser-ponyfill.js"}}</script>
<script src="{% static 'mfa/fido2.js' %}" type="module"></script>
4 changes: 2 additions & 2 deletions mfa/templates/mfa/create_FIDO2.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@
<button>Create</button>
</form>

<script src="{% static 'cbor-js/cbor.js' %}"></script>
<script src="{% static 'mfa/fido2.js' %}"></script>
<script type="importmap">{"imports": {"webauthn-json": "https://cdn.jsdelivr.net/npm/@github/[email protected]/dist/esm/webauthn-json.browser-ponyfill.js"}}</script>
<script src="{% static 'mfa/fido2.js' %}" type="module"></script>
2 changes: 1 addition & 1 deletion mfa/templates/mfa/create_TOTP.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% load static mfa %}
{% load mfa %}

<p>Scan the code with your TOTP app and enter a valid code to finish registration.</p>

Expand Down
2 changes: 1 addition & 1 deletion mfa/templates/mfa/create_recovery.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% load static mfa %}
{% load mfa %}

<p>A recovery code can be used when you lose access to your other two-factor authentication options. Each recovery code can only be used once.</p>
<p>Make sure to store it in a safe place! If you lose your login keys and don’t have the recovery codes you will lose access to your account.</p>
Expand Down
2 changes: 0 additions & 2 deletions tests/templates/registration/login.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
{% load i18n %}

<form method="POST">
{% csrf_token %}

Expand Down
6 changes: 0 additions & 6 deletions tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,12 +187,6 @@ def test_create(self):
res = self.client.get('/mfa/create/FIDO2/')
self.assertEqual(res.status_code, 200)

def test_encode(self):
self.assertEqual(fido2.encode({'foo': [1, 2]}), 'a163666f6f820102')

def test_decode(self):
self.assertEqual(fido2.decode('a163666f6f820102'), {'foo': [1, 2]})

def test_origin_https(self):
for domain, value, expected in [
('example.com', 'https://example.com', True),
Expand Down
Loading