Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prompt for revalidation of 2FA details. #147

Merged
merged 14 commits into from
May 11, 2023
Merged
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
37 changes: 37 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,37 @@ 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>
body.login-action-revalidate_2fa #login h1,
body.login-action-revalidate_2fa #backtoblog {
display: none;
}
</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; } );
80 changes: 80 additions & 0 deletions settings/src/components/revalidate-modal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* WordPress dependencies
*/
import { useContext, useEffect, useRef, useState } from '@wordpress/element';
import { GlobalContext } from '../script';
import { Button, Modal, __experimentalHStack as HStack } from '@wordpress/components';
adamwoodnz marked this conversation as resolved.
Show resolved Hide resolved
import { useMergeRefs, useFocusableIframe } from '@wordpress/compose';
import { refreshRecord } from '../utilities';

export default function RevalidateModal() {
const { clickScreenLink } = useContext( GlobalContext );
const [ showIframe, setIframe ] = useState( false );

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

if ( showIframe ) {
return (
<Modal title="Confirm your Two Factor" onRequestClose={ goBack }>
<RevalidateIframe />
</Modal>
);
}

return (
<Modal title="Confirm your Two Factor" onRequestClose={ goBack }>
<p>
Before you can update your Two Factor details, you first need to reconfirm your
existing login.
</p>
<HStack justify="right">
<Button variant="secondary" onClick={ goBack }>
Cancel
</Button>
<Button variant="primary" onClick={ showRevalidate }>
Continue
</Button>
</HStack>
</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 }
width="400px"
height="400px"
/>
</>
);
}
11 changes: 11 additions & 0 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 @@ -134,6 +138,13 @@ function Main( { userId } ) {
<AccountStatus />
</div>
);
} 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 = <RevalidateModal />;
} else {
screenContent = (
<Card>
Expand Down