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..b717b15d --- /dev/null +++ b/webauthn/manifest.json @@ -0,0 +1,26 @@ +{ + "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": { + } + }, + + "browser_specific_settings": { + "gecko": { + "id": "webauthn@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..6f0b6aa7 --- /dev/null +++ b/webauthn/popup.js @@ -0,0 +1,75 @@ +document.addEventListener('DOMContentLoaded', () => { + const registerButton = document.getElementById('registerButton'); + const authButton = document.getElementById('authButton'); + const optionsText = document.getElementById('optionsText'); + + 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.challenge = Uint8Array.fromBase64(options.challenge); + options.user.id = Uint8Array.fromBase64(options.user.id); + + 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.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 }); + 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); + } + }); +});