Step-by-Step Guide to building a custom two-factor authentication block for Gutenberg using Svelte standalone templates
Gutenberg Block Development Environment Setup
Before diving into the custom two-factor authentication (2FA) block for Gutenberg, a robust development environment is paramount. This setup leverages Node.js, npm, and the WordPress `@wordpress/scripts` package for efficient block compilation and management. Ensure you have Node.js (LTS recommended) and npm installed globally.
Navigate to your WordPress theme’s directory or a custom plugin directory where you intend to house your Gutenberg blocks. Initialize a new project using `npm init -y` to create a `package.json` file. Then, install the necessary WordPress scripts package:
npm init -y npm install @wordpress/scripts --save-dev
Next, configure your `package.json` to include build scripts. This allows for easy compilation of your Svelte components into JavaScript that WordPress can understand. Add the following scripts to your `package.json`:
{
"name": "custom-2fa-block",
"version": "1.0.0",
"description": "Custom 2FA Gutenberg Block",
"main": "build/index.js",
"scripts": {
"build": "wp-scripts build",
"start": "wp-scripts start",
"packages-update": "wp-scripts packages-update"
},
"keywords": ["wordpress", "gutenberg", "block", "svelte", "2fa"],
"author": "Antigravity",
"license": "GPL-2.0-or-later",
"devDependencies": {
"@wordpress/scripts": "^26.0.0"
}
}
Create a `src` directory at the root of your project. Inside `src`, create an `index.js` file. This will be the entry point for your Gutenberg block registration. For Svelte integration, we’ll need a build process that transpiles Svelte components. The `@wordpress/scripts` package can be configured to do this. You’ll typically need a `svelte.config.js` and a `jsconfig.json` (or `tsconfig.json` if using TypeScript).
Svelte Component Structure for 2FA Input
We will build a standalone Svelte component that handles the user input for the 2FA code. This component will be self-contained and easily integrated into the Gutenberg block editor. Create a `components` directory within your `src` folder and add a file named `TwoFactorInput.svelte`.
<!-- src/components/TwoFactorInput.svelte -->
<script>
import { createEventDispatcher } from 'svelte';
export let label = 'Two-Factor Authentication Code';
export let placeholder = 'Enter your code';
export let value = '';
const dispatch = createEventDispatcher();
function handleChange(event) {
value = event.target.value;
dispatch('input', value);
}
function handleBlur() {
dispatch('blur', value);
}
</script>
<div class="two-factor-input-wrapper">
<label>{label}</label>
<input
type="text"
bind:value="{value}"
placeholder="{placeholder}"
on:input="{handleChange}"
on:blur="{handleBlur}"
maxlength="6"
pattern="[0-9]*"
inputmode="numeric"
/>
</div>
<style>
.two-factor-input-wrapper {
margin-bottom: 1rem;
}
.two-factor-input-wrapper label {
display: block;
margin-bottom: 0.5rem;
font-weight: bold;
}
.two-factor-input-wrapper input {
width: 100%;
padding: 0.75rem;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
font-size: 1rem;
}
</style>
This Svelte component defines a reactive input field. It accepts `label`, `placeholder`, and `value` as props. It dispatches `input` and `blur` events, allowing the parent Gutenberg block to react to changes and validation. The `maxlength`, `pattern`, and `inputmode` attributes are optimized for numeric 2FA codes.
Gutenberg Block Registration and Svelte Integration
Now, let’s integrate this Svelte component into a Gutenberg block. We’ll use the `@wordpress/blocks` and `@wordpress/element` packages. The `index.js` file in your `src` directory will serve as the main entry point for registering the block.
// src/index.js
import { registerBlockType } from '@wordpress/blocks';
import { createElement } from '@wordpress/element';
import App from './App'; // We'll create this next
registerBlockType('custom-2fa/block', {
title: 'Custom 2FA Input',
icon: 'shield-alt', // Example icon
category: 'security',
attributes: {
twoFactorCode: {
type: 'string',
default: '',
},
},
edit: (props) => {
const { attributes, setAttributes } = props;
const handleCodeChange = (event) => {
setAttributes({ twoFactorCode: event.detail });
};
// Render the Svelte component within the Gutenberg editor
// This requires a Svelte renderer for the editor side.
// For simplicity here, we'll use a placeholder and explain the Svelte integration.
// In a real scenario, you'd compile Svelte to a JS module and import it.
return createElement('div', { className: 'custom-2fa-editor' },
createElement('p', {}, '2FA Input Block (Editor View)'),
// Placeholder for Svelte component rendering in editor
// This would involve mounting a Svelte component instance.
createElement('div', { id: 'svelte-2fa-editor-mount' })
);
},
save: (props) => {
const { attributes } = props;
// The 'save' function defines how the block's content is saved to the database.
// For dynamic blocks or blocks that interact with server-side logic,
// this might return null and rely on a render_callback.
// For a simple input, we might save the value or a placeholder.
return createElement('div', { className: 'custom-2fa-frontend' },
createElement('p', {}, '2FA Input Block (Frontend View)'),
// In a real scenario, this would render the Svelte component or its output.
// For a security-sensitive input, you likely wouldn't save the code itself.
// You might save a nonce or a flag indicating the block is present.
createElement('div', { className: 'custom-2fa-frontend-mount' })
);
},
});
The `edit` function is responsible for rendering the block in the Gutenberg editor. The `save` function determines what gets saved to the post content. For a 2FA input, we typically don’t save the actual code. Instead, the block might serve as a UI element that triggers a server-side validation process.
To actually render the Svelte component within Gutenberg, we need a mechanism to compile and load the Svelte code. This is where a build tool like Vite or Webpack, configured for Svelte, becomes essential. The `@wordpress/scripts` package can be extended to support Svelte compilation. A common approach is to use a Svelte preprocessor within the build configuration.
Advanced Svelte Compilation for Gutenberg
To enable Svelte compilation with `@wordpress/scripts`, you’ll typically need a `svelte.config.js` file and potentially modify the `webpack.config.js` that `@wordpress/scripts` uses internally (though extending it directly is often discouraged; a better approach is to use a tool that integrates well with it, or a separate build step). A simpler path is to use a tool like Vite, which has excellent Svelte support, and then configure its output to be compatible with WordPress block registration.
Let’s assume a Vite-based build process. You would create a `vite.config.js`:
// vite.config.js
import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';
import path from 'path';
export default defineConfig({
plugins: [svelte()],
build: {
outDir: 'build', // Matches @wordpress/scripts output directory
emptyOutDir: true,
lib: {
entry: path.resolve(__dirname, 'src/index.js'), // Your main block entry point
name: 'Custom2faBlock',
fileName: (format) => `index.${format}.js`,
formats: ['iife'], // Immediately Invoked Function Expression for WordPress
},
rollupOptions: {
// Make sure to externalize deps that shouldn't be bundled
// into your library (e.g., WordPress dependencies)
external: ['react', 'react-dom'], // Example, adjust as needed
output: {
// Provide global variables to use in the UMD build
// for externalized deps
globals: {
react: 'React',
'react-dom': 'ReactDOM',
},
},
},
},
});
And a `svelte.config.js`:
// svelte.config.js
import adapter from '@sveltejs/adapter-auto'; // Or a specific adapter if needed
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
adapter: adapter(),
// You might need to configure paths for WordPress integration
// paths: {
// base: '/wp-content/themes/your-theme/build', // Example
// }
},
// Add any Svelte preprocessors or other configurations here
};
export default config;
With Vite configured, you would modify your `package.json` scripts to use Vite for building:
{
"name": "custom-2fa-block",
"version": "1.0.0",
// ... other fields
"scripts": {
"build": "vite build",
"dev": "vite",
"preview": "vite preview"
},
// ... devDependencies
"devDependencies": {
"vite": "^5.0.0",
"svelte": "^4.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@sveltejs/adapter-auto": "^3.0.0",
"@wordpress/scripts": "^26.0.0" // Keep for potential wp-specific utils if needed
}
}
Now, when you run `npm run build`, Vite will compile your Svelte components and bundle them into `build/index.js` (and potentially other formats depending on your `vite.config.js`). This output JS file can then be enqueued by WordPress.
Server-Side Integration and Security Considerations
The Gutenberg block itself is primarily a client-side UI component. For 2FA to be effective, it must be validated on the server. This involves:
- AJAX Endpoint: Create a WordPress AJAX endpoint (using `wp_ajax_` hooks) that the frontend JavaScript (or the block’s editor script) can call to submit the 2FA code.
- Validation Logic: On the server, retrieve the submitted 2FA code, verify it against the user’s expected code (e.g., from a TOTP secret stored securely), and return a success or failure response.
- Session Management: If the 2FA code is valid, update the user’s session to indicate that 2FA has been successfully completed. This might involve setting a specific cookie or transient.
- Security Best Practices:
- Never store raw 2FA secrets in the database without strong encryption.
- Use HTTPS for all communication.
- Implement rate limiting on the AJAX endpoint to prevent brute-force attacks.
- Sanitize and validate all input.
- Ensure the AJAX endpoint checks user capabilities and nonces.
Here’s a PHP example for registering an AJAX action:
<?php
/**
* Plugin Name: Custom 2FA Block
* Description: Adds a custom 2FA input block.
* Version: 1.0
* Author: Antigravity
*/
function custom_2fa_block_enqueue_scripts() {
// Enqueue the compiled JavaScript file.
// Ensure the path is correct based on your build output.
wp_enqueue_script(
'custom-2fa-block-js',
plugins_url( 'build/index.js', __FILE__ ), // Adjust path if in theme
array( 'wp-blocks', 'wp-element', 'wp-i18n' ),
filemtime( plugin_dir_path( __FILE__ ) . 'build/index.js' ) // Cache busting
);
// Localize script for AJAX URL and nonce
wp_localize_script( 'custom-2fa-block-js', 'custom2fa', array(
'ajax_url' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'custom_2fa_verify_nonce' ),
) );
}
add_action( 'enqueue_block_editor_assets', 'custom_2fa_block_enqueue_scripts' );
// Also enqueue for frontend if the block is used there directly
// add_action( 'wp_enqueue_scripts', 'custom_2fa_block_enqueue_scripts' );
function custom_2fa_register_block() {
register_block_type( 'custom-2fa/block', array(
'editor_script' => 'custom-2fa-block-js',
// If you have a server-side rendering callback for the frontend:
// 'render_callback' => 'custom_2fa_render_frontend',
) );
}
add_action( 'init', 'custom_2fa_register_block' );
// AJAX handler for 2FA verification
function custom_2fa_verify_code() {
check_ajax_referer( 'custom_2fa_verify_nonce', 'nonce' );
if ( ! isset( $_POST['code'] ) || empty( $_POST['code'] ) ) {
wp_send_json_error( array( 'message' => '2FA code is required.' ) );
}
$submitted_code = sanitize_text_field( $_POST['code'] );
$user_id = get_current_user_id();
if ( ! $user_id ) {
wp_send_json_error( array( 'message' => 'User not logged in.' ) );
}
// --- Replace with your actual 2FA verification logic ---
// Example: Check against a stored TOTP secret or a temporary code.
// This is a placeholder and requires a robust implementation.
$is_valid = false; // Assume invalid by default
// $user_secret = get_user_meta( $user_id, '_2fa_secret', true );
// if ( $user_secret && verify_totp_code( $user_secret, $submitted_code ) ) {
// $is_valid = true;
// }
// --- End of placeholder logic ---
// For demonstration, let's simulate a valid code if it's '123456'
if ( $submitted_code === '123456' ) {
$is_valid = true;
}
if ( $is_valid ) {
// Mark user as 2FA authenticated for this session
// Example: Set a transient or update user meta
set_transient( '2fa_authenticated_' . $user_id, true, HOUR_IN_SECONDS );
wp_send_json_success( array( 'message' => '2FA code verified successfully.' ) );
} else {
wp_send_json_error( array( 'message' => 'Invalid 2FA code.' ) );
}
}
add_action( 'wp_ajax_custom_2fa_verify', 'custom_2fa_verify_code' );
// Add for non-logged-in users if applicable (e.g., login screen)
// add_action( 'wp_ajax_nopriv_custom_2fa_verify', 'custom_2fa_verify_code' );
// Function to verify TOTP (requires a library like PHPGangsta/PHPGangsta_GoogleAuthenticator)
// function verify_totp_code( $secret, $code ) {
// // Implementation details...
// return false;
// }
// Optional: Server-side rendering for the frontend if needed
// function custom_2fa_render_frontend( $attributes ) {
// // This function would return the HTML to render on the frontend.
// // For a 2FA input, you might render a form that submits to the AJAX endpoint.
// // You would likely need to enqueue frontend scripts here as well.
// return '<div class="custom-2fa-frontend-wrapper"><p>Please enter your 2FA code.</p></div>';
// }
?>
The frontend JavaScript (which would be part of your compiled Svelte application or a separate script enqueued for the frontend) would then use `fetch` or `XMLHttpRequest` to send the code to `custom_2fa_verify_code` via `wp_ajax_custom_2fa_verify`. The `wp_localize_script` function is crucial for passing the AJAX URL and nonce to the client-side JavaScript.
Frontend JavaScript for AJAX Communication
You’ll need JavaScript on the frontend (and potentially within the editor’s `edit` function if you want live preview of validation) to handle the submission of the 2FA code to the AJAX endpoint. This can be done within your main Svelte `App.svelte` component or a dedicated utility file.
// src/frontend.js (or integrated into App.svelte)
// Assuming 'custom2fa' object is available from wp_localize_script
// and your Svelte component dispatches an 'input' event.
function send2FACode(code) {
const data = new URLSearchParams();
data.append('action', 'custom_2fa_verify');
data.append('nonce', custom2fa.nonce);
data.append('code', code);
fetch(custom2fa.ajax_url, {
method: 'POST',
body: data,
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
},
})
.then(response => response.json())
.then(result => {
if (result.success) {
console.log('2FA Success:', result.data.message);
// Redirect user, update UI, etc.
window.location.reload(); // Example: reload after successful 2FA
} else {
console.error('2FA Error:', result.data.message);
// Display error message to the user
alert('Invalid 2FA code. Please try again.');
}
})
.catch(error => {
console.error('AJAX Request Failed:', error);
alert('An error occurred. Please try again later.');
});
}
// Example usage: If your Svelte component has an ID 'svelte-2fa-frontend-mount'
// and you have a way to trigger 'send2FACode' from your Svelte component.
// You might pass a callback function from the Gutenberg block's edit/save props.
// In src/App.svelte (or similar main component)
// ...
// <script>
// import TwoFactorInput from './components/TwoFactorInput.svelte';
// import { send2FACode } from './api'; // Assuming send2FACode is exported from api.js
// let currentCode = '';
// let isSubmitting = false;
// function handleCodeInput(event) {
// currentCode = event.detail;
// }
// function handleSubmit() {
// if (currentCode && !isSubmitting) {
// isSubmitting = true;
// send2FACode(currentCode)
// .then(response => {
// // Handle success/error from response
// alert(response.message);
// })
// .catch(error => {
// alert('Error: ' + error.message);
// })
// .finally(() => {
// isSubmitting = false;
// });
// }
// }
// </script>
// <div class="custom-2fa-app">
// <TwoFactorInput
// label="Enter your verification code"
// bind:value="{currentCode}"
// on:input="{handleCodeInput}"
// />
// <button on:click="{handleSubmit}" disabled="{isSubmitting || !currentCode}">
// {isSubmitting ? 'Verifying...' : 'Verify'}
// </button>
// </div>
// ...
This setup provides a foundation for a secure and customizable two-factor authentication mechanism within WordPress, leveraging the power of Svelte for a modern UI and Gutenberg for seamless integration.