Skip to content

Commit

Permalink
Only create backup codes when user initiated (#296)
Browse files Browse the repository at this point in the history
* Require explicit action to create backup codes

* Stop rendering code actions before codes are available

* Make code generation button more descriptive of the action

* Fix TOTP success navigation
  • Loading branch information
adamwoodnz authored Sep 2, 2024
1 parent cbf19f5 commit 6ad0c50
Show file tree
Hide file tree
Showing 2 changed files with 61 additions and 60 deletions.
115 changes: 56 additions & 59 deletions settings/src/components/backup-codes.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,9 @@ import DownloadButton from './download-button';
*/
export default function BackupCodes( { onSuccess = () => {} } ) {
const {
user: { backupCodesEnabled, hasPrimaryProvider, backupCodesRemaining },
backupCodesVerified,
user: { hasPrimaryProvider },
} = useContext( GlobalContext );
const [ regenerating, setRegenerating ] = useState( false );
const [ generating, setGenerating ] = useState( false );

// Prevent users from accessing directly through the URL.
if ( ! hasPrimaryProvider ) {
Expand All @@ -41,26 +40,21 @@ export default function BackupCodes( { onSuccess = () => {} } ) {
);
}

if (
! backupCodesEnabled ||
backupCodesRemaining === 0 ||
regenerating ||
! backupCodesVerified
) {
return <Setup setRegenerating={ setRegenerating } onSuccess={ onSuccess } />;
}

return <Manage setRegenerating={ setRegenerating } />;
return generating ? (
<Setup setGenerating={ setGenerating } onSuccess={ onSuccess } />
) : (
<Manage setGenerating={ setGenerating } />
);
}

/**
* Setup the Backup Codes provider.
*
* @param props
* @param props.setRegenerating
* @param props.setGenerating
* @param props.onSuccess
*/
function Setup( { setRegenerating, onSuccess } ) {
function Setup( { setGenerating, onSuccess } ) {
const {
setGlobalNotice,
user: { userRecord },
Expand Down Expand Up @@ -110,18 +104,14 @@ function Setup( { setRegenerating, onSuccess } ) {
// The codes have already been saved to usermeta, see `generateCodes()` above.
setBackupCodesVerified( true );
setGlobalNotice( 'Backup codes have been enabled.' );
setRegenerating( false );
setGenerating( false );
onSuccess();
} );

return (
<>
<div className="wporg-2fa__screen-intro">
<p>
Backup codes let you access your account if your primary two-factor
authentication method is unavailable, like if your phone is lost or stolen. Each
code can only be used once.
</p>
<IntroText />

<p>Please print the codes and keep them in a safe place.</p>

Expand All @@ -142,12 +132,6 @@ function Setup( { setRegenerating, onSuccess } ) {
<>
<CodeList codes={ backupCodes } />

<ButtonGroup>
<CopyToClipboardButton contents={ backupCodes } />
<PrintButton />
<DownloadButton codes={ backupCodes } />
</ButtonGroup>

<CheckboxControl
label="I have printed or saved these codes"
checked={ hasPrinted }
Expand All @@ -172,63 +156,74 @@ function Setup( { setRegenerating, onSuccess } ) {
}

/**
* Display a list of backup codes
* Display a list of backup codes and actions
*
* @param props
* @param props.codes
*/
function CodeList( { codes } ) {
return (
<div className="wporg-2fa__backup-codes-list">
{ ! codes.length && (
<p>
Generating backup codes...
<Spinner />
</p>
) }
const hasCodes = !! codes.length;

{ codes.length > 0 && (
<ol>
{ codes.map( ( code ) => {
return (
<li key={ code } className="wporg-2fa__token">
{ code.slice( 0, 4 ) + ' ' + code.slice( 4 ) }
</li>
);
} ) }
</ol>
return (
<>
<div className="wporg-2fa__backup-codes-list">
{ hasCodes ? (
<ol>
{ codes.map( ( code ) => {
return (
<li key={ code } className="wporg-2fa__token">
{ code.slice( 0, 4 ) + ' ' + code.slice( 4 ) }
</li>
);
} ) }
</ol>
) : (
<p>
<Spinner /> Generating backup codes...
</p>
) }
</div>
{ hasCodes && (
<ButtonGroup>
<CopyToClipboardButton contents={ codes } />
<PrintButton />
<DownloadButton codes={ codes } />
</ButtonGroup>
) }
</div>
</>
);
}

const IntroText = () => (
<p>
Backup codes let you access your account if your primary two-factor authentication method is
unavailable, like if your phone is lost or stolen. Each code can only be used once.
</p>
);

/**
* Render the screen where users can manage Backup Codes.
*
* @param props
* @param props.setRegenerating
* @param props.setGenerating
*/
function Manage( { setRegenerating } ) {
function Manage( { setGenerating } ) {
const {
user: { backupCodesRemaining },
user: { backupCodesEnabled, backupCodesRemaining },
} = useContext( GlobalContext );

return (
<>
<div className="wporg-2fa__screen-intro">
<p>
Backup codes let you access your account if your primary two-factor
authentication method is unavailable, like if your phone is lost or stolen. Each
code can only be used once.
</p>
<IntroText />

{ backupCodesRemaining > 5 && (
{ backupCodesEnabled && backupCodesRemaining > 5 && (
<p>
You have <strong>{ backupCodesRemaining }</strong> backup codes remaining.
</p>
) }

{ backupCodesRemaining <= 5 && (
{ backupCodesEnabled && backupCodesRemaining <= 5 && (
<Notice status="warning" isDismissible={ false }>
<Icon icon={ warning } />
<div>
Expand All @@ -241,8 +236,10 @@ function Manage( { setRegenerating } ) {
) }
</div>

<Button isSecondary onClick={ () => setRegenerating( true ) }>
Generate new backup codes
<Button isSecondary onClick={ () => setGenerating( true ) }>
{ backupCodesEnabled
? 'Regenerate and save backup codes'
: 'Generate and save backup codes' }
</Button>
</>
);
Expand Down
6 changes: 5 additions & 1 deletion settings/src/components/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ import { GlobalContext } from '../script';
* Render the correct component based on the URL.
*/
export default function Settings() {
const { backupCodesEnabled, navigateToScreen, screen } = useContext( GlobalContext );
const {
user: { backupCodesEnabled },
navigateToScreen,
screen,
} = useContext( GlobalContext );

// The index is the URL slug and the value is the React component.
const components = {
Expand Down

0 comments on commit 6ad0c50

Please sign in to comment.