From 6a4c632fd2830bcdd3dfe34ef914b665ef69ee06 Mon Sep 17 00:00:00 2001 From: Richard Bloor Date: Mon, 6 Apr 2026 05:42:08 +1200 Subject: [PATCH 1/2] Example to illustrate the use of the Web Authentication API --- examples.json | 9 +++- webauthn/README.md | 29 +++++++++++++ webauthn/manifest.json | 30 +++++++++++++ webauthn/popup.css | 23 ++++++++++ webauthn/popup.html | 33 ++++++++++++++ webauthn/popup.js | 99 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 221 insertions(+), 2 deletions(-) create mode 100644 webauthn/README.md create mode 100644 webauthn/manifest.json create mode 100644 webauthn/popup.css create mode 100644 webauthn/popup.html create mode 100644 webauthn/popup.js diff --git a/examples.json b/examples.json index 702fa86b..a742d880 100644 --- a/examples.json +++ b/examples.json @@ -407,6 +407,11 @@ ], "name": "notify-link-clicks-i18n" }, + { + "description": "Demonstrates the use of protocol handlers.", + "javascript_apis": [], + "name": "open-irc-links" + }, { "description": "Adds a browser action icon to the toolbar. When the browser action is clicked, the add-on opens a page that was packaged with it.", "javascript_apis": ["browserAction.onClicked", "tabs.create"], @@ -621,8 +626,8 @@ "name": "window-manipulator" }, { - "description": "Demonstrates the use of protocol handlers.", + "description": "Demonstrates how to use the Web Authentication API.", "javascript_apis": [], - "name": "open-irc-links" + "name": "webauthn" } ] diff --git a/webauthn/README.md b/webauthn/README.md new file mode 100644 index 00000000..b05225be --- /dev/null +++ b/webauthn/README.md @@ -0,0 +1,29 @@ +# webauthn + +This extension illustrates the use of [navigator.credentials.create()](https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer/create) and [navigator.credentials.get()](https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer/get) to create and validate credentials that a website can use to authenticate a user. + +## What it does ## + +The extension includes an action with a popup that includes HTML, CSS, and JavaScript. + +When you click the action (toolbar button), the extension's popup opens, enabling you to: + +* Paste a JSON file. +* Click a button to register the JSON using `navigator.credentials.create()`. +* Click a button to authenticate the JSON using `navigator.credentials.get()`. + + +When you click a button, the JavaScript reads the JSON file and, if needed, converts the challenge and user ID to an ArrayBuffer. It then runs the selected `navigator.credentials` method. + +If you choose to register the JSON, you get either a confirmation or an error message, depending on the outcome. + +If you choose to authenticate JSON, you get either details of the credential ID, authenticator data, client data JSON, and signature, or an error message if the authentication fails. + +## What it shows ## + +In this example, you see how to: + +* Use an action (toolbar button) with a popup. +* Give a popup style and behavior using CSS and JavaScript. +* Convert strings in base64 to an ArrayBuffer. +* Execute `navigator.credentials.create()` and `navigator.credentials.get()`. diff --git a/webauthn/manifest.json b/webauthn/manifest.json new file mode 100644 index 00000000..bfb507e6 --- /dev/null +++ b/webauthn/manifest.json @@ -0,0 +1,30 @@ +{ + "description": "Registers and authenticates WebAuthn credentials using options from the popover.", + "manifest_version": 3, + "name": "WebAuthn extension", + "version": "1.0", + "homepage_url": "https://github.com/mdn/webextensions-examples/tree/master/webauthn", + + "action": { + "default_popup": "popup.html", + "default_icon": { + } + }, + + "permissions": [ + "storage" + ], + + "browser_specific_settings": { + "gecko": { + "id": "beastify@mozilla.org", + "data_collection_permissions": { + "required": ["none"] + } + } + }, + + "host_permissions": [ + "https://*/*" + ] +} diff --git a/webauthn/popup.css b/webauthn/popup.css new file mode 100644 index 00000000..2b80c855 --- /dev/null +++ b/webauthn/popup.css @@ -0,0 +1,23 @@ +body { + width: 400px; /* Set the desired width */ + height: 300px; /* Set the desired height */ + padding: 10px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-family: Arial, sans-serif; +} + +textarea { + width: 100%; + height: 100px; + margin-bottom: 10px; +} + +button { + width: 100%; + padding: 10px; + margin: 5px 0; + cursor: pointer; +} \ No newline at end of file diff --git a/webauthn/popup.html b/webauthn/popup.html new file mode 100644 index 00000000..c9823ac0 --- /dev/null +++ b/webauthn/popup.html @@ -0,0 +1,33 @@ + + + + + WebAuthn extension + + + + +

WebAuthn

+ + + + + + diff --git a/webauthn/popup.js b/webauthn/popup.js new file mode 100644 index 00000000..afbcc749 --- /dev/null +++ b/webauthn/popup.js @@ -0,0 +1,99 @@ +document.addEventListener('DOMContentLoaded', () => { + const registerButton = document.getElementById('registerButton'); + const authButton = document.getElementById('authButton'); + const optionsText = document.getElementById('optionsText'); + + // Helper function to convert a Base64 string to an ArrayBuffer + function base64ToArrayBuffer(base64) { + const binaryString = window.atob(base64); + const len = binaryString.length; + const bytes = new Uint8Array(len); + for (let i = 0; i < len; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return bytes.buffer; + } + + // Convert relevant options properties if they are Base64 strings + function convertOptions(options) { + // Convert challenge if it's a string + if (typeof options.challenge === 'string') { + options.challenge = base64ToArrayBuffer(options.challenge); + } + if (options.allowCredentials) { + options.allowCredentials = options.allowCredentials.map(cred => ({ + ...cred, + id: base64ToArrayBuffer(cred.id) + })); + } + // Optionally convert user.id if it's a string (depending on your use-case) + if (options.user && typeof options.user.id === 'string') { + options.user.id = base64ToArrayBuffer(options.user.id); + } + return options; + } + + function arrayBufferToBase64(buffer) { + return btoa(String.fromCharCode(...new Uint8Array(buffer))); + } + + // Handle WebAuthn registration + registerButton.addEventListener('click', async () => { + let options; + try { + options = JSON.parse(optionsText.value); + } catch (error) { + alert('Invalid JSON in text field'); + return; + } + + // Convert challenge (and user id if necessary) + options = convertOptions(options); + + try { + const credential = await navigator.credentials.create({ publicKey: options }); + console.log('Credential created:', credential); + alert('Credential created successfully'); + } catch (err) { + console.error('Error during credential creation:', err); + alert('Error during credential creation: ' + err); + } + }); + + + + // Handle WebAuthn authentication + authButton.addEventListener('click', async () => { + let options; + try { + options = JSON.parse(optionsText.value); + } catch (error) { + alert('Invalid JSON in text field'); + return; + } + + // Convert challenge if necessary for authentication options as well + options = convertOptions(options); + + try { + const assertion = await navigator.credentials.get({ publicKey: options }); + console.log('Assertion obtained:', assertion); + + const credentialId = assertion.id; + const authenticatorData = arrayBufferToBase64(assertion.response.authenticatorData); + const clientDataJSON = new TextDecoder().decode(assertion.response.clientDataJSON); + const signature = arrayBufferToBase64(assertion.response.signature); + + alert( + `Assertion obtained successfully!\n\n` + + `Credential ID: ${credentialId}\n\n` + + `Authenticator Data: ${authenticatorData}\n\n` + + `Client Data JSON: ${clientDataJSON}\n\n` + + `Signature: ${signature}` + ); + } catch (err) { + console.error('Error during credential assertion:', err); + alert('Error during credential assertion: ' + err); + } + }); +}); From 0547e3b8cc401c84ab4b41cd2c1a972498ee5d39 Mon Sep 17 00:00:00 2001 From: Richard Bloor Date: Tue, 21 Apr 2026 11:40:51 +1200 Subject: [PATCH 2/2] Updates following feedback --- webauthn/manifest.json | 6 +----- webauthn/popup.js | 40 ++++++++-------------------------------- 2 files changed, 9 insertions(+), 37 deletions(-) diff --git a/webauthn/manifest.json b/webauthn/manifest.json index bfb507e6..b717b15d 100644 --- a/webauthn/manifest.json +++ b/webauthn/manifest.json @@ -11,13 +11,9 @@ } }, - "permissions": [ - "storage" - ], - "browser_specific_settings": { "gecko": { - "id": "beastify@mozilla.org", + "id": "webauthn@mozilla.org", "data_collection_permissions": { "required": ["none"] } diff --git a/webauthn/popup.js b/webauthn/popup.js index afbcc749..6f0b6aa7 100644 --- a/webauthn/popup.js +++ b/webauthn/popup.js @@ -3,36 +3,6 @@ document.addEventListener('DOMContentLoaded', () => { const authButton = document.getElementById('authButton'); const optionsText = document.getElementById('optionsText'); - // Helper function to convert a Base64 string to an ArrayBuffer - function base64ToArrayBuffer(base64) { - const binaryString = window.atob(base64); - const len = binaryString.length; - const bytes = new Uint8Array(len); - for (let i = 0; i < len; i++) { - bytes[i] = binaryString.charCodeAt(i); - } - return bytes.buffer; - } - - // Convert relevant options properties if they are Base64 strings - function convertOptions(options) { - // Convert challenge if it's a string - if (typeof options.challenge === 'string') { - options.challenge = base64ToArrayBuffer(options.challenge); - } - if (options.allowCredentials) { - options.allowCredentials = options.allowCredentials.map(cred => ({ - ...cred, - id: base64ToArrayBuffer(cred.id) - })); - } - // Optionally convert user.id if it's a string (depending on your use-case) - if (options.user && typeof options.user.id === 'string') { - options.user.id = base64ToArrayBuffer(options.user.id); - } - return options; - } - function arrayBufferToBase64(buffer) { return btoa(String.fromCharCode(...new Uint8Array(buffer))); } @@ -48,7 +18,8 @@ document.addEventListener('DOMContentLoaded', () => { } // Convert challenge (and user id if necessary) - options = convertOptions(options); + options.challenge = Uint8Array.fromBase64(options.challenge); + options.user.id = Uint8Array.fromBase64(options.user.id); try { const credential = await navigator.credentials.create({ publicKey: options }); @@ -73,7 +44,12 @@ document.addEventListener('DOMContentLoaded', () => { } // Convert challenge if necessary for authentication options as well - options = convertOptions(options); + options.challenge = Uint8Array.fromBase64(options.challenge); + if (Array.isArray(options?.allowCredentials)) { + for (const ac of options.allowCredentials) { + ac.id = Uint8Array.fromBase64(ac.id); + } + } try { const assertion = await navigator.credentials.get({ publicKey: options });