Step-by-Step Guide to building a custom two-factor authentication block for Gutenberg using Next.js headless configurations
Setting Up the Headless WordPress Environment
Before diving into Gutenberg block development, a robust headless WordPress setup is paramount. This involves configuring WordPress to serve content via its REST API and setting up a separate frontend application, in this case, built with Next.js. We’ll assume a standard WordPress installation with the REST API enabled (which is default). The critical part is ensuring your WordPress installation is accessible for API requests and that your Next.js application can authenticate with it, especially for sensitive operations like user login and 2FA verification.
For authentication, we’ll leverage WordPress’s built-in nonce system and potentially a custom API endpoint for handling the 2FA process. This requires a plugin that exposes user authentication endpoints or custom code within your theme’s `functions.php` or a custom plugin.
Next.js Frontend Configuration for API Interaction
Your Next.js application will act as the client. It needs to make authenticated requests to your WordPress backend. We’ll use `fetch` or a library like `axios` for this. Environment variables are crucial for managing your WordPress API URL and any necessary API keys or secrets.
Environment Variables
Create a .env.local file in the root of your Next.js project:
NEXT_PUBLIC_WORDPRESS_API_URL=https://your-wordpress-site.com/wp-json/wp/v2 WORDPRESS_API_SECRET_KEY=your_secret_key_for_api_access
The NEXT_PUBLIC_ prefix makes the WordPress API URL available on the client-side. For sensitive keys used server-side (e.g., for creating users or managing roles), do not prefix with NEXT_PUBLIC_.
Gutenberg Block Development: The Two-Factor Authentication Component
We’ll build a custom Gutenberg block that will serve as the UI for the two-factor authentication process. This block will need to handle user input for the 2FA code and communicate with your backend API.
Block Registration and Structure
In your WordPress theme or a custom plugin, you’ll register the block. This involves a JavaScript file that defines the block’s attributes, editor interface, and frontend rendering.
// src/blocks/two-factor-auth/index.js
import { registerBlockType } from '@wordpress/blocks';
import { Edit } from './edit';
import { save } from './save';
registerBlockType('custom/two-factor-auth', {
title: 'Two-Factor Authentication',
icon: 'shield-alt', // Dashicon slug
category: 'security',
attributes: {
// Define attributes if needed, e.g., for API endpoint configuration
},
edit: Edit,
save: save,
});
Editor Component (edit.js)
This component will be rendered within the Gutenberg editor. It should provide a user-friendly interface for configuring the block (if necessary) and a preview of the 2FA input field.
// src/blocks/two-factor-auth/edit.js
import { __ } from '@wordpress/i18n';
import { useBlockProps } from '@wordpress/block-editor';
import { TextControl, Button, Spinner } from '@wordpress/components';
import { useState } from '@wordpress/element';
export const Edit = () => {
const blockProps = useBlockProps();
const [twoFactorCode, setTwoFactorCode] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [message, setMessage] = useState('');
const handleVerifyCode = async () => {
setIsLoading(true);
setMessage('');
try {
// This is a placeholder. The actual API call will be more complex,
// involving nonces and potentially user context.
const response = await fetch('/wp-admin/admin-ajax.php', { // Example using WP AJAX
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
action: 'verify_two_factor_code', // Custom AJAX action
code: twoFactorCode,
_ajax_nonce: 'YOUR_AJAX_NONCE', // Needs to be dynamically generated
}),
});
const result = await response.json();
if (result.success) {
setMessage(__('Verification successful!', 'your-text-domain'));
} else {
setMessage(__('Verification failed. Please check your code.', 'your-text-domain'));
}
} catch (error) {
console.error('Error verifying code:', error);
setMessage(__('An error occurred. Please try again.', 'your-text-domain'));
} finally {
setIsLoading(false);
}
};
return (
{__('Two-Factor Authentication', 'your-text-domain')}
{__('Enter your two-factor authentication code below.', 'your-text-domain')}
setTwoFactorCode(value)}
disabled={isLoading}
/>
{message && {message}
}
);
};
Save Component (save.js)
The save function determines how the block is rendered on the frontend. For dynamic blocks that require JavaScript to function, you might return null and rely on a server-side rendering (SSR) approach or a client-side JavaScript that hydrates the static HTML.
// src/blocks/two-factor-auth/save.js
import { useBlockProps } from '@wordpress/block-editor';
export const save = () => {
// For dynamic blocks that rely on client-side JS, you can return null
// and handle rendering entirely in the edit component's client-side logic
// or via a separate JavaScript file that targets this block's wrapper.
// Alternatively, for a truly static save, you'd render the HTML structure here.
// For this example, we'll assume client-side hydration.
const blockProps = useBlockProps.save();
return (
{/* Placeholder for client-side rendering */}
);
};
Backend Implementation: WordPress AJAX and API Endpoints
The core logic for verifying the 2FA code will reside in your WordPress backend. We'll use WordPress's AJAX API for simplicity in this example, but for a headless setup, dedicated REST API endpoints are generally preferred.
WordPress AJAX Handler
Add the following to your theme's functions.php or a custom plugin:
// functions.php or custom-plugin.php
add_action('wp_ajax_verify_two_factor_code', 'handle_verify_two_factor_code');
add_action('wp_ajax_nopriv_verify_two_factor_code', 'handle_verify_two_factor_code'); // If anonymous users can trigger this
function handle_verify_two_factor_code() {
// 1. Verify nonce for security
check_ajax_referer('verify_two_factor_nonce', '_ajax_nonce'); // Ensure this matches the nonce sent from JS
// 2. Get the submitted code
$submitted_code = isset($_POST['code']) ? sanitize_text_field($_POST['code']) : '';
// 3. Retrieve the user's actual 2FA secret (this depends on your 2FA plugin/implementation)
// For demonstration, let's assume a user is logged in and their 2FA secret is stored.
// In a real headless scenario, you'd likely be verifying against a token or session.
$user_id = get_current_user_id();
if (!$user_id) {
wp_send_json_error(['message' => 'User not logged in.']);
wp_die();
}
// --- Placeholder for actual 2FA verification logic ---
// This part is highly dependent on your chosen 2FA solution (e.g., Google Authenticator, Authy, etc.)
// You would typically use a library like PHPGangsta_GoogleAuthenticator or similar.
// Example using a hypothetical function `verify_google_authenticator_code`:
$user_secret = get_user_meta($user_id, 'google_2fa_secret', true); // Example meta key
if (empty($user_secret)) {
wp_send_json_error(['message' => '2FA secret not configured for this user.']);
wp_die();
}
// Include the Google Authenticator library (ensure it's properly loaded)
require_once 'path/to/PHPGangsta_GoogleAuthenticator.php'; // Adjust path
$authenticator = new PHPGangsta_GoogleAuthenticator();
$is_valid = $authenticator->verifyCode($user_secret, $submitted_code, 2); // 2 is the allowed time window
if ($is_valid) {
// 4. If valid, perform actions: e.g., set a flag, log the user in, generate a session token
// For headless, you might generate a JWT or a custom API token here.
// Example: Update user meta to indicate 2FA is passed for this session
update_user_meta($user_id, 'two_factor_verified_session', time());
wp_send_json_success(['message' => '2FA code verified successfully.']);
} else {
wp_send_json_error(['message' => 'Invalid 2FA code.']);
}
wp_die(); // Always include this at the end of AJAX handlers
}
// Function to generate and output the AJAX nonce
function get_ajax_nonce_for_block() {
return wp_create_nonce('verify_two_factor_nonce');
}
// You'll need to pass this nonce to your JavaScript.
// One way is to enqueue a script and pass it as a localized variable.
add_action('enqueue_block_editor_assets', function() {
wp_enqueue_script(
'custom-two-factor-auth-editor-script',
get_template_directory_uri() . '/src/blocks/two-factor-auth/editor.js', // Path to your compiled JS
array('wp-blocks', 'wp-element', 'wp-components', 'wp-i18n'),
filemtime(get_template_directory() . '/src/blocks/two-factor-auth/editor.js')
);
wp_localize_script('custom-two-factor-auth-editor-script', 'customTwoFactorAuth', array(
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => get_ajax_nonce_for_block(),
));
});
Important Considerations for Backend:
- Nonce Security: Always use
check_ajax_referer()to prevent CSRF attacks. The nonce must be generated server-side and passed to the client. - User Context: In a headless setup, you'll likely be dealing with API tokens or JWTs instead of WordPress's logged-in user session. The verification logic needs to adapt to this. You might need to pass a token with the AJAX request and verify it server-side to identify the user.
- 2FA Implementation: The example uses a placeholder for Google Authenticator. You must integrate with your actual 2FA provider's SDK or library.
- REST API Endpoints: For a cleaner headless architecture, consider creating custom REST API endpoints using
register_rest_routeinstead of AJAX. This aligns better with stateless API principles.
Frontend Hydration and Interaction (Next.js)
The Gutenberg block, when rendered on the frontend, will output static HTML. To make it interactive, you need to "hydrate" it with your Next.js application's JavaScript. This involves fetching the necessary data and rendering the interactive component.
Fetching WordPress Content and Initializing the Block
In your Next.js page component, you'll fetch content from WordPress. When the page loads, you'll identify where your Gutenberg block is rendered and initialize the interactive 2FA component.
// pages/your-page-with-2fa-block.js (Next.js)
import { useEffect, useState } from 'react';
import Head from 'next/head';
// Assume you have a component to render the 2FA form
import TwoFactorAuthForm from '../components/TwoFactorAuthForm';
export default function PageWith2FA({ wordpressPageContent }) {
const [is2FASectionPresent, setIs2FASectionPresent] = useState(false);
const [wpApiUrl, setWpApiUrl] = useState('');
const [wpNonce, setWpNonce] = useState('');
useEffect(() => {
// Check if the 2FA block is rendered in the content
// This is a simplified check; you might need more robust parsing
const isPresent = wordpressPageContent.content.rendered.includes('custom-two-factor-auth');
setIs2FASectionPresent(isPresent);
// Fetch nonce from WordPress REST API if available
// This requires a custom endpoint in WordPress to expose the nonce
const fetchNonce = async () => {
try {
const response = await fetch(`${process.env.NEXT_PUBLIC_WORDPRESS_API_URL}/custom/v1/get-2fa-nonce`);
const data = await response.json();
if (data.nonce) {
setWpNonce(data.nonce);
}
} catch (error) {
console.error("Error fetching nonce:", error);
}
};
if (isPresent) {
fetchNonce();
setWpApiUrl(process.env.NEXT_PUBLIC_WORDPRESS_API_URL);
}
}, [wordpressPageContent]);
return (
{wordpressPageContent.title.rendered}
{/* Render WordPress content */}
{/* Conditionally render the interactive 2FA form */}
{is2FASectionPresent && wpNonce && (
)}
);
}
// Example of fetching page data using getStaticProps or getServerSideProps
export async function getStaticProps() {
const res = await fetch(`${process.env.NEXT_PUBLIC_WORDPRESS_API_URL}/pages?slug=your-page-slug`);
const pageData = await res.json();
if (!pageData || pageData.length === 0) {
return {
notFound: true,
};
}
return {
props: {
wordpressPageContent: pageData[0],
},
revalidate: 10, // Re-generate the page at most once every 10 seconds
};
}
Interactive 2FA Component (components/TwoFactorAuthForm.js)
This React component will handle the user interaction for entering the 2FA code and communicating with your WordPress backend.
// components/TwoFactorAuthForm.js
import { useState } from 'react';
const TwoFactorAuthForm = ({ apiUrl, nonce }) => {
const [twoFactorCode, setTwoFactorCode] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [message, setMessage] = useState('');
const handleVerifyCode = async (e) => {
e.preventDefault();
setIsLoading(true);
setMessage('');
try {
// Use WordPress AJAX endpoint for verification
const response = await fetch('/wp-admin/admin-ajax.php', { // Or a custom REST API endpoint
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
// Add Authorization header if using JWT or custom tokens
// 'Authorization': `Bearer ${yourAuthToken}`
},
body: new URLSearchParams({
action: 'verify_two_factor_code', // Matches the AJAX action in PHP
code: twoFactorCode,
_ajax_nonce: nonce, // The nonce fetched from WordPress
}),
});
const result = await response.json();
if (result.success) {
setMessage('Verification successful! You are now logged in.');
// Redirect user or update UI state
} else {
setMessage(result.data?.message || 'Verification failed. Please check your code.');
}
} catch (error) {
console.error('Error verifying code:', error);
setMessage('An error occurred. Please try again.');
} finally {
setIsLoading(false);
}
};
return (
Two-Factor Authentication
Please enter the code from your authenticator app.
{message && {message}
}
);
};
export default TwoFactorAuthForm;
Advanced Considerations and Security Best Practices
Building a secure 2FA system requires meticulous attention to detail. Here are some advanced points:
Rate Limiting and Brute-Force Protection
Implement rate limiting on your verification endpoint (both AJAX and REST API) to prevent brute-force attacks. This can be done at the server level (e.g., using Nginx or a WAF) or within your WordPress plugin logic.
Secure Storage of 2FA Secrets
If you are managing 2FA secrets server-side (e.g., for Google Authenticator), ensure they are stored securely. Avoid storing them in plain text. Encryption at rest is recommended.
Session Management in Headless
After successful 2FA verification, you need a secure way to manage the user's session on the frontend. This typically involves issuing a JWT (JSON Web Token) or a secure, HTTP-only cookie that your Next.js application can use for subsequent authenticated requests to your WordPress backend.
Custom REST API Endpoints
For a truly headless architecture, replace the AJAX calls with custom REST API endpoints. This provides a cleaner separation of concerns and is more aligned with stateless API design.
// Example custom REST API endpoint for nonce
add_action('rest_api_init', function () {
register_rest_route('custom/v1', '/get-2fa-nonce', array(
'methods' => 'GET',
'callback' => 'rest_api_get_2fa_nonce',
'permission_callback' => '__return_true', // Adjust permissions as needed
));
});
function rest_api_get_2fa_nonce() {
$nonce = wp_create_nonce('verify_two_factor_nonce'); // Use the same nonce action
return new WP_REST_Response(array('nonce' => $nonce), 200);
}
// Example custom REST API endpoint for verification
add_action('rest_api_init', function () {
register_rest_route('custom/v1', '/verify-2fa', array(
'methods' => 'POST',
'callback' => 'rest_api_verify_2fa',
'permission_callback' => '__return_true', // Authentication handled within the callback
));
});
function rest_api_verify_2fa(WP_REST_Request $request) {
// Verify nonce passed in headers or body
$nonce = $request->get_header('X-WP-Nonce') ?: $request->get_param('_ajax_nonce');
if (!wp_verify_nonce($nonce, 'verify_two_factor_nonce')) {
return new WP_Error('rest_invalid_nonce', 'Invalid nonce', array('status' => 403));
}
$submitted_code = $request->get_param('code');
$user_id = get_current_user_id(); // Assumes user is already authenticated via token/cookie
if (!$user_id) {
return new WP_Error('rest_not_authenticated', 'User not authenticated', array('status' => 401));
}
// ... (Actual 2FA verification logic as in AJAX handler) ...
if ($is_valid) {
// Generate JWT or set session cookie
return new WP_REST_Response(array('message' => '2FA verified'), 200);
} else {
return new WP_Error('rest_invalid_code', 'Invalid 2FA code', array('status' => 400));
}
}
By following these steps, you can construct a secure and functional two-factor authentication block within your headless WordPress setup, leveraging the power of Gutenberg and Next.js.