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
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";