- commit
- 44be1fd05bad2783daa8d6a85aa6a25417e30093
- parent
- fbbc19dcaa50b50e40e5476ed774d48b056cbe42
- Author
- Tobias Bengfort <tobias.bengfort@posteo.de>
- Date
- 2025-05-21 14:41
Merge pull request #13 from xi/fido2-json fido2: use JSON encoding
Diffstat
| M | README.md | 2 | +- |
| M | mfa/methods/fido2.py | 44 | +++++++++++++------------------------------- |
| M | mfa/static/mfa/fido2.js | 84 | ++++++++++++++++++++++++------------------------------------- |
| A | mfa/static/mfa/vendor/webauthn-json.browser-ponyfill.js | 243 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| M | mfa/templates/mfa/auth_FIDO2.html | 3 | +-- |
| M | mfa/templates/mfa/create_FIDO2.html | 3 | +-- |
| M | tests/tests.py | 6 | ------ |
7 files changed, 292 insertions, 93 deletions
diff --git a/README.md b/README.md
@@ -29,7 +29,7 @@ pip install django-mfa3 29 29 `settings.py` for a full list of settings. 30 30 4. Register URLs: `path('mfa/', include('mfa.urls', namespace='mfa')` 31 31 5. The included templates are just examples, so you should [replace them](https://docs.djangoproject.com/en/stable/howto/overriding-templates/) with your own32 -1 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)).-1 32 6. FIDO2 requires client side code. You can either implement it yourself or use the included fido2.js. 33 33 7. Somewhere in your app, add a link to `'mfa:list'` 34 34 35 35 ## Enforce MFA
diff --git a/mfa/methods/fido2.py b/mfa/methods/fido2.py
@@ -1,32 +1,23 @@1 -1 from fido2 import cbor-1 1 import json -1 2 2 3 from fido2.features import webauthn_json_mapping 3 4 from fido2.server import Fido2Server 4 5 from fido2.utils import websafe_decode 5 6 from fido2.utils import websafe_encode6 -1 from fido2.webauthn import AttestationObject7 7 from fido2.webauthn import AttestedCredentialData8 -1 from fido2.webauthn import AuthenticatorData9 -1 from fido2.webauthn import CollectedClientData10 8 from fido2.webauthn import PublicKeyCredentialRpEntity -1 9 from fido2.webauthn import PublicKeyCredentialUserEntity 11 10 12 11 from .. import settings 13 12 14 13 name = 'FIDO2' 15 1416 -1 webauthn_json_mapping.enabled = False-1 15 webauthn_json_mapping.enabled = True 17 16 fido2 = Fido2Server( 18 17 PublicKeyCredentialRpEntity(id=settings.DOMAIN, name=settings.SITE_TITLE), 19 18 ) 20 19 21 2022 -1 def encode(data):23 -1 return cbor.encode(data).hex()24 -125 -126 -1 def decode(s):27 -1 return cbor.decode(bytes.fromhex(s))28 -129 -130 21 def get_credentials(user): 31 22 keys = user.mfakey_set.filter(method=name) 32 23 return [AttestedCredentialData(websafe_decode(key.secret)) for key in keys] @@ -34,24 +25,19 @@ def get_credentials(user): 34 25 35 26 def register_begin(user): 36 27 registration_data, state = fido2.register_begin(37 -1 {38 -1 'id': str(user.id).encode('utf-8'),39 -1 'name': user.get_username(),40 -1 'displayName': user.get_full_name(),41 -1 },-1 28 PublicKeyCredentialUserEntity( -1 29 id=str(user.id).encode('utf-8'), -1 30 name=user.get_username(), -1 31 display_name=user.get_full_name(), -1 32 ), 42 33 get_credentials(user), 43 34 user_verification=settings.FIDO2_USER_VERIFICATION, 44 35 )45 -1 return encode(registration_data), state-1 36 return json.dumps(dict(registration_data)), state 46 37 47 38 48 39 def register_complete(state, request_data):49 -1 data = decode(request_data)50 -1 auth_data = fido2.register_complete(51 -1 state,52 -1 CollectedClientData(data['clientData']),53 -1 AttestationObject(data['attestationObject']),54 -1 )-1 40 auth_data = fido2.register_complete(state, json.loads(request_data)) 55 41 return websafe_encode(auth_data.credential_data) 56 42 57 43 @@ -61,16 +47,12 @@ def authenticate_begin(user): 61 47 credentials, 62 48 user_verification=settings.FIDO2_USER_VERIFICATION, 63 49 )64 -1 return encode(auth_data), state-1 50 return json.dumps(dict(auth_data)), state 65 51 66 52 67 53 def authenticate_complete(state, user, request_data):68 -1 data = decode(request_data)69 54 fido2.authenticate_complete( 70 55 state, 71 56 get_credentials(user),72 -1 data['credentialId'],73 -1 CollectedClientData(data['clientData']),74 -1 AuthenticatorData(data['authenticatorData']),75 -1 data['signature'],-1 57 json.loads(request_data), 76 58 )
diff --git a/mfa/static/mfa/fido2.js b/mfa/static/mfa/fido2.js
@@ -1,56 +1,38 @@1 -1 (function() {2 -1 var encode = function(data) {3 -1 var buffer = CBOR.encode(data);4 -1 var arr = new Uint8Array(buffer);5 -1 return arr.reduce((s, b) => s + b.toString(16).padStart(2, '0'), '');6 -1 };-1 1 import * as webauthnJSON from './vendor/webauthn-json.browser-ponyfill.js'; 7 28 -1 var decode = function(hex) {9 -1 var arr = new Uint8Array(hex.length / 2);10 -1 for (var i = 0; i < arr.length; i += 1) {11 -1 arr[i] = parseInt(hex.substring(i * 2, i * 2 + 2), 16);12 -1 }13 -1 return CBOR.decode(arr.buffer);14 -1 };-1 3 var initCreate = function() { -1 4 var form = document.querySelector('form[data-fido2-create]'); -1 5 if (form) { -1 6 var options = webauthnJSON.parseCreationOptionsFromJSON( -1 7 JSON.parse(form.dataset.fido2Create) -1 8 ); -1 9 form.addEventListener('submit', function(event) { -1 10 event.preventDefault(); 15 1116 -1 var initCreate = function() {17 -1 var form = document.querySelector('form[data-fido2-create]');18 -1 if (form) {19 -1 var options = decode(form.dataset.fido2Create);20 -1 form.addEventListener('submit', function(event) {21 -1 event.preventDefault();-1 12 webauthnJSON.create(options).then(attestation => { -1 13 this.code.value = JSON.stringify(attestation); -1 14 form.submit(); -1 15 }).catch(alert); -1 16 }); -1 17 } -1 18 }; 22 1923 -1 navigator.credentials.create(options).then(attestation => {24 -1 this.code.value = encode({25 -1 'attestationObject': new Uint8Array(attestation.response.attestationObject),26 -1 'clientData': new Uint8Array(attestation.response.clientDataJSON),27 -1 });28 -1 form.submit();29 -1 }).catch(alert);30 -1 });31 -1 }32 -1 };-1 20 var initAuth = function() { -1 21 var form = document.querySelector('form[data-fido2-auth]'); -1 22 if (form) { -1 23 var options = webauthnJSON.parseRequestOptionsFromJSON( -1 24 JSON.parse(form.dataset.fido2Auth) -1 25 ); -1 26 form.addEventListener('submit', function(event) { -1 27 event.preventDefault(); 33 2834 -1 var initAuth = function() {35 -1 var form = document.querySelector('form[data-fido2-auth]');36 -1 if (form) {37 -1 var options = decode(form.dataset.fido2Auth);38 -1 form.addEventListener('submit', function(event) {39 -1 event.preventDefault();-1 29 webauthnJSON.get(options).then(assertion => { -1 30 this.code.value = JSON.stringify(assertion); -1 31 form.submit(); -1 32 }).catch(alert); -1 33 }); -1 34 } -1 35 }; 40 3641 -1 navigator.credentials.get(options).then(assertion => {42 -1 this.code.value = encode({43 -1 'credentialId': new Uint8Array(assertion.rawId),44 -1 'authenticatorData': new Uint8Array(assertion.response.authenticatorData),45 -1 'clientData': new Uint8Array(assertion.response.clientDataJSON),46 -1 'signature': new Uint8Array(assertion.response.signature),47 -1 });48 -1 form.submit();49 -1 }).catch(alert);50 -1 });51 -1 }52 -1 };53 -154 -1 initCreate();55 -1 initAuth();56 -1 })();-1 37 initCreate(); -1 38 initAuth();
diff --git a/mfa/static/mfa/vendor/webauthn-json.browser-ponyfill.js b/mfa/static/mfa/vendor/webauthn-json.browser-ponyfill.js
@@ -0,0 +1,243 @@
-1 1 /*!
-1 2 * @github/webauthn-json 2.1.1
-1 3 * Copyright (c) 2019 GitHub, Inc.
-1 4 *
-1 5 * Permission is hereby granted, free of charge, to any person obtaining a copy
-1 6 * of this software and associated documentation files (the "Software"), to
-1 7 * deal in the Software without restriction, including without limitation the
-1 8 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
-1 9 * sell copies of the Software, and to permit persons to whom the Software is
-1 10 * furnished to do so, subject to the following conditions:
-1 11 *
-1 12 * The above copyright notice and this permission notice shall be included in
-1 13 * all copies or substantial portions of the Software.
-1 14 *
-1 15 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-1 16 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-1 17 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-1 18 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-1 19 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-1 20 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
-1 21 * IN THE SOFTWARE.
-1 22 */
-1 23
-1 24 // src/webauthn-json/base64url.ts
-1 25 function base64urlToBuffer(baseurl64String) {
-1 26 const padding = "==".slice(0, (4 - baseurl64String.length % 4) % 4);
-1 27 const base64String = baseurl64String.replace(/-/g, "+").replace(/_/g, "/") + padding;
-1 28 const str = atob(base64String);
-1 29 const buffer = new ArrayBuffer(str.length);
-1 30 const byteView = new Uint8Array(buffer);
-1 31 for (let i = 0; i < str.length; i++) {
-1 32 byteView[i] = str.charCodeAt(i);
-1 33 }
-1 34 return buffer;
-1 35 }
-1 36 function bufferToBase64url(buffer) {
-1 37 const byteView = new Uint8Array(buffer);
-1 38 let str = "";
-1 39 for (const charCode of byteView) {
-1 40 str += String.fromCharCode(charCode);
-1 41 }
-1 42 const base64String = btoa(str);
-1 43 const base64urlString = base64String.replace(/\+/g, "-").replace(
-1 44 /\//g,
-1 45 "_"
-1 46 ).replace(/=/g, "");
-1 47 return base64urlString;
-1 48 }
-1 49
-1 50 // src/webauthn-json/convert.ts
-1 51 var copyValue = "copy";
-1 52 var convertValue = "convert";
-1 53 function convert(conversionFn, schema, input) {
-1 54 if (schema === copyValue) {
-1 55 return input;
-1 56 }
-1 57 if (schema === convertValue) {
-1 58 return conversionFn(input);
-1 59 }
-1 60 if (schema instanceof Array) {
-1 61 return input.map((v) => convert(conversionFn, schema[0], v));
-1 62 }
-1 63 if (schema instanceof Object) {
-1 64 const output = {};
-1 65 for (const [key, schemaField] of Object.entries(schema)) {
-1 66 if (schemaField.derive) {
-1 67 const v = schemaField.derive(input);
-1 68 if (v !== void 0) {
-1 69 input[key] = v;
-1 70 }
-1 71 }
-1 72 if (!(key in input)) {
-1 73 if (schemaField.required) {
-1 74 throw new Error(`Missing key: ${key}`);
-1 75 }
-1 76 continue;
-1 77 }
-1 78 if (input[key] == null) {
-1 79 output[key] = null;
-1 80 continue;
-1 81 }
-1 82 output[key] = convert(
-1 83 conversionFn,
-1 84 schemaField.schema,
-1 85 input[key]
-1 86 );
-1 87 }
-1 88 return output;
-1 89 }
-1 90 }
-1 91 function derived(schema, derive) {
-1 92 return {
-1 93 required: true,
-1 94 schema,
-1 95 derive
-1 96 };
-1 97 }
-1 98 function required(schema) {
-1 99 return {
-1 100 required: true,
-1 101 schema
-1 102 };
-1 103 }
-1 104 function optional(schema) {
-1 105 return {
-1 106 required: false,
-1 107 schema
-1 108 };
-1 109 }
-1 110
-1 111 // src/webauthn-json/basic/schema.ts
-1 112 var publicKeyCredentialDescriptorSchema = {
-1 113 type: required(copyValue),
-1 114 id: required(convertValue),
-1 115 transports: optional(copyValue)
-1 116 };
-1 117 var simplifiedExtensionsSchema = {
-1 118 appid: optional(copyValue),
-1 119 appidExclude: optional(copyValue),
-1 120 credProps: optional(copyValue)
-1 121 };
-1 122 var simplifiedClientExtensionResultsSchema = {
-1 123 appid: optional(copyValue),
-1 124 appidExclude: optional(copyValue),
-1 125 credProps: optional(copyValue)
-1 126 };
-1 127 var credentialCreationOptions = {
-1 128 publicKey: required({
-1 129 rp: required(copyValue),
-1 130 user: required({
-1 131 id: required(convertValue),
-1 132 name: required(copyValue),
-1 133 displayName: required(copyValue)
-1 134 }),
-1 135 challenge: required(convertValue),
-1 136 pubKeyCredParams: required(copyValue),
-1 137 timeout: optional(copyValue),
-1 138 excludeCredentials: optional([publicKeyCredentialDescriptorSchema]),
-1 139 authenticatorSelection: optional(copyValue),
-1 140 attestation: optional(copyValue),
-1 141 extensions: optional(simplifiedExtensionsSchema)
-1 142 }),
-1 143 signal: optional(copyValue)
-1 144 };
-1 145 var publicKeyCredentialWithAttestation = {
-1 146 type: required(copyValue),
-1 147 id: required(copyValue),
-1 148 rawId: required(convertValue),
-1 149 authenticatorAttachment: optional(copyValue),
-1 150 response: required({
-1 151 clientDataJSON: required(convertValue),
-1 152 attestationObject: required(convertValue),
-1 153 transports: derived(
-1 154 copyValue,
-1 155 (response) => {
-1 156 var _a;
-1 157 return ((_a = response.getTransports) == null ? void 0 : _a.call(response)) || [];
-1 158 }
-1 159 )
-1 160 }),
-1 161 clientExtensionResults: derived(
-1 162 simplifiedClientExtensionResultsSchema,
-1 163 (pkc) => pkc.getClientExtensionResults()
-1 164 )
-1 165 };
-1 166 var credentialRequestOptions = {
-1 167 mediation: optional(copyValue),
-1 168 publicKey: required({
-1 169 challenge: required(convertValue),
-1 170 timeout: optional(copyValue),
-1 171 rpId: optional(copyValue),
-1 172 allowCredentials: optional([publicKeyCredentialDescriptorSchema]),
-1 173 userVerification: optional(copyValue),
-1 174 extensions: optional(simplifiedExtensionsSchema)
-1 175 }),
-1 176 signal: optional(copyValue)
-1 177 };
-1 178 var publicKeyCredentialWithAssertion = {
-1 179 type: required(copyValue),
-1 180 id: required(copyValue),
-1 181 rawId: required(convertValue),
-1 182 authenticatorAttachment: optional(copyValue),
-1 183 response: required({
-1 184 clientDataJSON: required(convertValue),
-1 185 authenticatorData: required(convertValue),
-1 186 signature: required(convertValue),
-1 187 userHandle: required(convertValue)
-1 188 }),
-1 189 clientExtensionResults: derived(
-1 190 simplifiedClientExtensionResultsSchema,
-1 191 (pkc) => pkc.getClientExtensionResults()
-1 192 )
-1 193 };
-1 194
-1 195 // src/webauthn-json/basic/api.ts
-1 196 function createRequestFromJSON(requestJSON) {
-1 197 return convert(base64urlToBuffer, credentialCreationOptions, requestJSON);
-1 198 }
-1 199 function createResponseToJSON(credential) {
-1 200 return convert(
-1 201 bufferToBase64url,
-1 202 publicKeyCredentialWithAttestation,
-1 203 credential
-1 204 );
-1 205 }
-1 206 function getRequestFromJSON(requestJSON) {
-1 207 return convert(base64urlToBuffer, credentialRequestOptions, requestJSON);
-1 208 }
-1 209 function getResponseToJSON(credential) {
-1 210 return convert(
-1 211 bufferToBase64url,
-1 212 publicKeyCredentialWithAssertion,
-1 213 credential
-1 214 );
-1 215 }
-1 216
-1 217 // src/webauthn-json/basic/supported.ts
-1 218 function supported() {
-1 219 return !!(navigator.credentials && navigator.credentials.create && navigator.credentials.get && window.PublicKeyCredential);
-1 220 }
-1 221
-1 222 // src/webauthn-json/browser-ponyfill.ts
-1 223 async function create(options) {
-1 224 const response = await navigator.credentials.create(
-1 225 options
-1 226 );
-1 227 response.toJSON = () => createResponseToJSON(response);
-1 228 return response;
-1 229 }
-1 230 async function get(options) {
-1 231 const response = await navigator.credentials.get(
-1 232 options
-1 233 );
-1 234 response.toJSON = () => getResponseToJSON(response);
-1 235 return response;
-1 236 }
-1 237 export {
-1 238 create,
-1 239 get,
-1 240 createRequestFromJSON as parseCreationOptionsFromJSON,
-1 241 getRequestFromJSON as parseRequestOptionsFromJSON,
-1 242 supported
-1 243 };
diff --git a/mfa/templates/mfa/auth_FIDO2.html b/mfa/templates/mfa/auth_FIDO2.html
@@ -14,5 +14,4 @@ 14 14 <a href="{% url 'mfa:auth' 'recovery' %}">Use recovery code instead</a> 15 15 </form> 16 1617 -1 <script src="{% static 'cbor-js/cbor.js' %}"></script>18 -1 <script src="{% static 'mfa/fido2.js' %}"></script>-1 17 <script src="{% static 'mfa/fido2.js' %}" type="module"></script>
diff --git a/mfa/templates/mfa/create_FIDO2.html b/mfa/templates/mfa/create_FIDO2.html
@@ -15,5 +15,4 @@ 15 15 <button>Create</button> 16 16 </form> 17 1718 -1 <script src="{% static 'cbor-js/cbor.js' %}"></script>19 -1 <script src="{% static 'mfa/fido2.js' %}"></script>-1 18 <script src="{% static 'mfa/fido2.js' %}" type="module"></script>
diff --git a/tests/tests.py b/tests/tests.py
@@ -187,12 +187,6 @@ class FIDO2Test(MFATestCase): 187 187 res = self.client.get('/mfa/create/FIDO2/') 188 188 self.assertEqual(res.status_code, 200) 189 189190 -1 def test_encode(self):191 -1 self.assertEqual(fido2.encode({'foo': [1, 2]}), 'a163666f6f820102')192 -1193 -1 def test_decode(self):194 -1 self.assertEqual(fido2.decode('a163666f6f820102'), {'foo': [1, 2]})195 -1196 190 def test_origin_https(self): 197 191 for domain, value, expected in [ 198 192 ('example.com', 'https://example.com', True),