Skip to content

Commit

Permalink
Prompt for revalidation of 2FA details before changing 2FA details. (#…
Browse files Browse the repository at this point in the history
…147)

* Switch to using an Iframe for the revalidation.
* Speed up the UI by assuming the 2FA is successful while the user record is async refresed.
* Use single modal flow to match design
* Style modal and iframe contents from login page
* Display the account status screen behind the modal

---------

Co-authored-by: Adam Wood <[email protected]>
  • Loading branch information
dd32 and adamwoodnz authored May 11, 2023
1 parent 12a8458 commit a1fa9e5
Show file tree
Hide file tree
Showing 6 changed files with 230 additions and 27 deletions.
18 changes: 18 additions & 0 deletions settings/rest-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,24 @@ function register_user_fields(): void {
],
]
);

register_rest_field(
'user',
'2fa_revalidation',
[
'get_callback' => function( $user ) {
$revalidate_url = Two_Factor_Core::get_user_two_factor_revalidate_url( true );
$expiry = apply_filters( 'two_factor_revalidate_time', 10 * MINUTE_IN_SECONDS, $user['id'], '' );
$expires_at = Two_Factor_Core::is_current_user_session_two_factor() + $expiry;

return compact( 'revalidate_url', 'expires_at' );
},
'schema' => [
'type' => 'array',
'context' => [ 'edit' ],
],
]
);
}

/**
Expand Down
74 changes: 74 additions & 0 deletions settings/settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ function replace_core_ui_with_custom() : void {
remove_action( 'edit_user_profile_update', array( 'Two_Factor_Core', 'user_two_factor_options_update' ) );

add_action( 'bbp_user_edit_account', __NAMESPACE__ . '\render_custom_ui' );

// Add some customizations to the revalidate_2fa page for when it's displayed in an iframe.
add_action( 'login_footer', __NAMESPACE__ . '\login_footer_revalidate_customizations' );
}

/**
Expand Down Expand Up @@ -63,3 +66,74 @@ function render_custom_ui() : void {

echo do_blocks( "<!-- wp:wporg-two-factor/settings $json_attrs /-->" );
}

function login_footer_revalidate_customizations() {
// When the revalidate_2fa page is displayed in an interim login on not-login, add some style and JS handlers.
if (
'login.wordpress.org' === $_SERVER['HTTP_HOST'] ||
empty( $_REQUEST['interim-login'] ) ||
'revalidate_2fa' !== ( $_REQUEST['action'] ?? '' )
) {
return;
}

?>
<style>
.login-action-revalidate_2fa {
background: white;
padding: 0 32px;
}

.login-action-revalidate_2fa #login {
padding: unset;
width: auto;
}

.login-action-revalidate_2fa #login h1,
.login-action-revalidate_2fa #backtoblog,
.login-action-revalidate_2fa .two-factor-prompt + br {
display: none;
}

.login-action-revalidate_2fa #login_error {
box-shadow: none;
background-color: #f4a2a2;
}

.login-action-revalidate_2fa #loginform {
border: none;
padding: 0;
box-shadow: none;
margin-top: 0;
overflow: visible;
}

.login-action-revalidate_2fa #loginform .button-primary {
width: 100%;
float: unset;
}

.login-action-revalidate_2fa #login p {
font-size: 14px;
}

.login-action-revalidate_2fa .backup-methods-wrap {
padding: 0;
}
</style>
<script>
(function() {
const loginFormExists = !! document.querySelector( '#loginform' );
const loginFormMessage = document.querySelector( '#login .message' )?.textContent || '';

// If the login no longer exists, let the parent know.
if ( ! loginFormExists ) {
window.parent.postMessage( { type: 'reValidationComplete', message: loginFormMessage }, '*' );
}
})();
</script>
<?php
}

// To test, revalidate every 30seconds.
// add_filter( 'two_factor_revalidate_time', function() { return 30; } );
62 changes: 62 additions & 0 deletions settings/src/components/revalidate-modal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* WordPress dependencies
*/
import { useContext, useEffect, useRef } from '@wordpress/element';
import { GlobalContext } from '../script';
import { Modal } from '@wordpress/components';
import { useMergeRefs, useFocusableIframe } from '@wordpress/compose';
import { refreshRecord } from '../utilities';

export default function RevalidateModal() {
const { clickScreenLink } = useContext( GlobalContext );

const goBack = ( event ) => clickScreenLink( event, 'account-status' );

return (
<Modal
title="Two-Factor Authentication"
onRequestClose={ goBack }
className="wporg-2fa__revalidate-modal"
>
<p>To update your two-factor options, you must first revalidate your session.</p>

<RevalidateIframe />
</Modal>
);
}

function RevalidateIframe() {
const { setGlobalNotice, userRecord } = useContext( GlobalContext );
const ref = useRef();

useEffect( () => {
function maybeRefreshUser( { data: { type, message } = {} } ) {
if ( type !== 'reValidationComplete' ) {
return;
}

setGlobalNotice( message || 'Two-Factor confirmed' );

// Pretend that the expires_at is in the future (+1hr), this provides a 'faster' UI.
// This intentionally doesn't use `edit()` to prevent it attempting to update it on the server.
userRecord.record[ '2fa_revalidation' ].expires_at = new Date().getTime() / 1000 + 3600;

// Refresh the user record, to fetch the correct 2fa_revalidation data.
refreshRecord( userRecord );
}

window.addEventListener( 'message', maybeRefreshUser );

return () => {
window.removeEventListener( 'message', maybeRefreshUser );
};
}, [] );

return (
<iframe
title="Two-Factor Revalidation"
ref={ useMergeRefs( [ ref, useFocusableIframe() ] ) }
src={ userRecord.record[ '2fa_revalidation' ].revalidate_url }
/>
);
}
34 changes: 34 additions & 0 deletions settings/src/components/revalidate-modal.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
.wporg-2fa__revalidate-modal {
$header-height: 100px;

&.components-modal__frame {
border-radius: 8px;
}

.components-modal__header {
height: $header-height;

h1 {
margin: unset;
}
}

.components-modal__content {
margin-top: $header-height;
padding: 0;
}

p {
margin: 0 32px 1rem;
// Match style of login page text in iframe
font-size: 14px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
}

iframe {
border: none;
width: 100%;
// Allow for error messages above form
height: 330px;
}
}
68 changes: 41 additions & 27 deletions settings/src/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import EmailAddress from './components/email-address';
import TOTP from './components/totp';
import BackupCodes from './components/backup-codes';
import GlobalNotice from './components/global-notice';
import RevalidateModal from './components/revalidate-modal';

export const GlobalContext = createContext( null );

Expand Down Expand Up @@ -67,6 +68,9 @@ function Main( { userId } ) {
'backup-codes': BackupCodes,
};

// The screens where a recent two factor challenge is required.
const twoFactorRequiredScreens = [ 'totp', 'backup-codes' ];

let initialScreen = currentUrl.searchParams.get( 'screen' );

if ( ! components[ initialScreen ] ) {
Expand Down Expand Up @@ -126,40 +130,50 @@ function Main( { userId } ) {
return <Spinner />;
}

let screenContent;
let screenContent = (
<Card>
<CardHeader className="wporg-2fa__navigation" size="xSmall">
<ScreenLink
screen="account-status"
ariaLabel="Back to the account status page"
anchorText={
<>
<Icon icon={ chevronLeft } />
Back
</>
}
/>

<h3>
{ screen.replace( '-', ' ' ).replace( 'totp', 'Two-Factor Authentication' ) }
</h3>
</CardHeader>

<CardBody className={ 'wporg-2fa__' + screen }>
<CurrentScreen />
</CardBody>
</Card>
);

if ( 'account-status' === screen ) {
screenContent = (
<div className={ 'wporg-2fa__' + screen }>
<div className={ 'wporg-2fa__account-status' }>
<AccountStatus />
</div>
);
} else {
} else if (
twoFactorRequiredScreens.includes( screen ) &&
userRecord.record[ '2fa_available_providers' ] &&
userRecord.record[ '2fa_revalidation' ] &&
userRecord.record[ '2fa_revalidation' ].expires_at <= new Date().getTime() / 1000
) {
screenContent = (
<Card>
<CardHeader className="wporg-2fa__navigation" size="xSmall">
<ScreenLink
screen="account-status"
ariaLabel="Back to the account status page"
anchorText={
<>
<Icon icon={ chevronLeft } />
Back
</>
}
/>

<h3>
{ screen
.replace( '-', ' ' )
.replace( 'totp', 'Two-Factor Authentication' ) }
</h3>
</CardHeader>

<CardBody className={ 'wporg-2fa__' + screen }>
<CurrentScreen />
</CardBody>
</Card>
<>
<div className={ 'wporg-2fa__account-status' }>
<AccountStatus />
</div>
<RevalidateModal />
</>
);
}

Expand Down
1 change: 1 addition & 0 deletions settings/src/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,4 @@
@import "components/global-notice";
@import "components/screen-link";
@import "components/auto-tabbing-input";
@import "components/revalidate-modal";

0 comments on commit a1fa9e5

Please sign in to comment.