From cc47baab3deecd7581e73d62cd9f87c66d6bf5d5 Mon Sep 17 00:00:00 2001 From: Ian Dunn Date: Mon, 29 May 2023 08:45:06 -0700 Subject: [PATCH 1/3] Load Settings script last so that dependencies are available --- settings/src/block.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings/src/block.json b/settings/src/block.json index 0d411767..6a9b177d 100644 --- a/settings/src/block.json +++ b/settings/src/block.json @@ -17,7 +17,7 @@ "textdomain": "wporg", "editorScript": "file:./index.js", "editorStyle": "file:./index.css", - "viewScript": [ "file:./script.js", "zxcvbn-async", "two-factor-qr-code-generator" ], + "viewScript": [ "zxcvbn-async", "two-factor-qr-code-generator", "file:./script.js" ], "style": [ "file:./style-index.css", "wp-components" ], "render": "file:./render.php" } From 1a08d9265e67c075d0bb457e0a45b7871a2b243a Mon Sep 17 00:00:00 2001 From: Ian Dunn Date: Mon, 29 May 2023 15:23:50 -0700 Subject: [PATCH 2/3] Use `userRecord` to decouple `onRegisterSuccess` and `ListKeys` --- settings/src/components/webauthn/webauthn.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/settings/src/components/webauthn/webauthn.js b/settings/src/components/webauthn/webauthn.js index 2286707f..772aeff5 100644 --- a/settings/src/components/webauthn/webauthn.js +++ b/settings/src/components/webauthn/webauthn.js @@ -29,16 +29,18 @@ export default function WebAuthn() { // and then replace userkeys with the value returned by the call // probably also refreshRecord( userRecord ); const onRegisterSuccess = useCallback( () => { - userKeys.push( { - id: 1111, + const newKeys = userRecord.record[ '2fa_webauthn_keys' ].push( { + id: Math.random(), name: 'New Key', } ); + + userRecord.edit( { '2fa_webauthn_keys': newKeys } ); setStep( 'success' ); if ( ! backupCodesEnabled ) { // todo redirect to backup codes } - }, [ userKeys ] ); + }, [ userRecord.record[ '2fa_webauthn_keys' ] ] ); return ( <> From 5561e3002327f9dc73ab533825e6009eef2a1820 Mon Sep 17 00:00:00 2001 From: Ian Dunn Date: Mon, 29 May 2023 15:26:48 -0700 Subject: [PATCH 3/3] WebAuthn: Add dynamic functionality to Delete key stub --- settings/rest-api.php | 6 +- settings/src/block.json | 2 +- settings/src/components/webauthn/list-keys.js | 148 +++++++++++++++--- settings/src/components/webauthn/webauthn.js | 17 +- .../src/components/webauthn/webauthn.scss | 7 + 5 files changed, 145 insertions(+), 35 deletions(-) diff --git a/settings/rest-api.php b/settings/rest-api.php index 7e979e72..0b5063a5 100644 --- a/settings/rest-api.php +++ b/settings/rest-api.php @@ -213,9 +213,11 @@ function register_user_fields(): void { 'get_callback' => function( $user ) { $keys = WebAuthn_Credential_Store::get_user_keys( get_userdata( $user['id'] ) ); - // Remove sensitive and unnecessary data. array_walk( $keys, function( & $key ) { - $key = array_intersect_key( (array) $key, array_flip( [ 'id', 'name' ] ) ); + $key->delete_nonce = wp_create_nonce( 'delete-key_' . $key->credential_id ); + + // Remove unnecessary data. + $key = array_intersect_key( (array) $key, array_flip( [ 'id', 'credential_id', 'name', 'delete_nonce' ] ) ); } ); return $keys; diff --git a/settings/src/block.json b/settings/src/block.json index 6a9b177d..d3e151e2 100644 --- a/settings/src/block.json +++ b/settings/src/block.json @@ -17,7 +17,7 @@ "textdomain": "wporg", "editorScript": "file:./index.js", "editorStyle": "file:./index.css", - "viewScript": [ "zxcvbn-async", "two-factor-qr-code-generator", "file:./script.js" ], + "viewScript": [ "wp-util", "zxcvbn-async", "two-factor-qr-code-generator", "file:./script.js" ], "style": [ "file:./style-index.css", "wp-components" ], "render": "file:./render.php" } diff --git a/settings/src/components/webauthn/list-keys.js b/settings/src/components/webauthn/list-keys.js index 7ce107fc..6e3b9095 100644 --- a/settings/src/components/webauthn/list-keys.js +++ b/settings/src/components/webauthn/list-keys.js @@ -1,38 +1,134 @@ /** * WordPress dependencies */ -import { Button } from '@wordpress/components'; +import { Button, Modal, Notice, Spinner } from '@wordpress/components'; +import { useCallback, useContext, useState } from '@wordpress/element'; +import { Icon, cancelCircleFilled } from '@wordpress/icons'; -const confirm = window.confirm; +/** + * Internal dependencies + */ +import { GlobalContext } from '../../script'; +import { refreshRecord } from '../../utilities'; + +/** + * Global dependencies + */ +const ajax = wp.ajax; /** * Render the list of keys. + */ +export default function ListKeys() { + const { + user: { userRecord }, + setGlobalNotice, + } = useContext( GlobalContext ); + const keys = userRecord.record[ '2fa_webauthn_keys' ]; + + const [ modalKey, setModalKey ] = useState( null ); + const [ modalError, setModalError ] = useState( null ); + const [ deleting, setDeleting ] = useState( false ); + + /** + * After the user confirms their intent, POST an AJAX request to remove a key. + */ + const onConfirmDelete = useCallback( async () => { + setDeleting( true ); + + try { + await ajax.post( 'webauthn_delete_key', { + user_id: userRecord.record.id, + handle: modalKey.credential_id, + _ajax_nonce: modalKey.delete_nonce, + } ); + + setGlobalNotice( modalKey.name + ' has been deleted.' ); + setModalKey( null ); + refreshRecord( userRecord ); + } catch ( error ) { + // The endpoint returns some errors as a string, but others as an object. + setModalError( error?.responseJSON?.data || error ); + } finally { + setDeleting( false ); + } + }, [ modalKey ] ); + + return ( + <> +
    + { keys.map( ( key ) => ( +
  • + { key.name } + + +
  • + ) ) } +
+ + { modalKey && ( + setModalKey( null ) } + onConfirm={ onConfirmDelete } + /> + ) } + + ); +} + +/** + * Prompt the user to confirm they want to delete a key. * - * @param props - * @param props.keys + * @param {Object} props + * @param {Object} props.keyToRemove + * @param {Function} props.onConfirm + * @param {Function} props.onClose + * @param {boolean} props.deleting + * @param {string} props.error */ -export default function ListKeys( { keys } ) { +function ConfirmRemoveKey( { keyToRemove, onConfirm, onClose, deleting, error } ) { return ( -
    - { keys.map( ( key ) => ( -
  • - { key.name } - - { /* todo add onclick handler that pops up a to confirm. maybe pass in from parent? */ } - -
  • - ) ) } -
+ +

+ Are you sure you want to remove the { keyToRemove.name } security key? +

+ +
+ + + +
+ + { deleting && ( +

+ +

+ ) } + + { error && ( + + + { error } + + ) } +
); } diff --git a/settings/src/components/webauthn/webauthn.js b/settings/src/components/webauthn/webauthn.js index 772aeff5..71459b2b 100644 --- a/settings/src/components/webauthn/webauthn.js +++ b/settings/src/components/webauthn/webauthn.js @@ -11,6 +11,9 @@ import { Icon, check } from '@wordpress/icons'; import { GlobalContext } from '../../script'; import ListKeys from './list-keys'; +/** + * Global dependencies + */ const confirm = window.confirm; /** @@ -20,7 +23,6 @@ export default function WebAuthn() { const { user: { userRecord }, } = useContext( GlobalContext ); - const userKeys = userRecord.record[ '2fa_webauthn_keys' ]; const backupCodesEnabled = userRecord.record[ '2fa_available_providers' ].includes( 'Two_Factor_Backup_Codes' ); const [ step, setStep ] = useState( 'manage' ); @@ -44,10 +46,12 @@ export default function WebAuthn() { return ( <> - { 'manage' === step && } + { 'manage' === step && } { 'register' === step && ( setStep( 'waiting' ) } /> + // convert to named func that makes api call to register key + // handle failure ) } { 'waiting' === step && ( @@ -64,9 +68,8 @@ export default function WebAuthn() { * * @param props * @param props.setStep - * @param props.userKeys */ -function Manage( { setStep, userKeys } ) { +function Manage( { setStep } ) { return ( <>

@@ -77,7 +80,8 @@ function Manage( { setStep, userKeys } ) {

Security Keys

- + +