Skip to content

Commit f3ede1f

Browse files
authored
Merge pull request #840 from WordPress/add/AGENTS.md
Add AGENTS.md, TESTS.md and CLAUDE.md
2 parents f3b454d + 84132b7 commit f3ede1f

File tree

3 files changed

+270
-0
lines changed

3 files changed

+270
-0
lines changed

AGENTS.md

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# AI Instructions
2+
3+
Two-Factor is a WordPress plugin, potentially eventually merging into WordPress Core, that provides Multi-Factor Authentication for WordPress interactive logins. It is network-enabled and can be activated across a WordPress multisite network.
4+
5+
## Development Environment
6+
7+
Requires Docker. Uses `@wordpress/env` to run a local WordPress install in containers.
8+
9+
```bash
10+
npm install
11+
npm run build
12+
npm run env start
13+
```
14+
15+
For code coverage support: `npm run env start -- --xdebug=coverage`
16+
17+
`npm test` and `npm run composer` are wrappers that execute commands inside the `tests-cli` wp-env container at the plugin path. Tests must be run through these wrappers, not directly with `phpunit`.
18+
19+
## Commands
20+
21+
### Testing
22+
23+
@TESTS.md
24+
25+
### Linting & Static Analysis
26+
27+
```bash
28+
npm run lint # all linters (PHP, CSS, JS)
29+
npm run lint:php # PHPCS with WordPress + VIP-Go standards
30+
npm run lint:phpstan # PHPStan static analysis (level 0)
31+
npm run lint:css # wp-scripts lint-style
32+
npm run lint:js # wp-scripts lint-js
33+
npm run format # auto-fix PHPCS and JS/CSS issues
34+
```
35+
36+
### Build
37+
38+
```bash
39+
npm run build
40+
```
41+
42+
The Grunt build copies all distributable files to `dist/` (respecting `.distignore`) and copies `node_modules/qrcode-generator/qrcode.js` into `dist/includes/`. The `qrcode-generator` package is a **runtime JS dependency** — it is not present in `includes/` in the source tree and must be built before the plugin is usable in a browser context. Always run `npm run build` after a fresh checkout.
43+
44+
## Architecture
45+
46+
The plugin follows a provider pattern. `Two_Factor_Core` owns the login interception and orchestration; individual providers handle their own credential prompts and validation.
47+
48+
### Core Files
49+
50+
- **`two-factor.php`** — Entry point. Defines `TWO_FACTOR_DIR` and `TWO_FACTOR_VERSION`, loads all core files, instantiates `Two_Factor_Compat`, and calls `Two_Factor_Core::add_hooks()`.
51+
- **`class-two-factor-core.php`** — Central class. Owns the login flow, user meta, nonce management, rate limiting, session tracking, REST API endpoints, and the user profile settings UI.
52+
- **`class-two-factor-compat.php`** — Compatibility shims for third-party plugins (currently: Jetpack SSO). New integrations go here; the goal is to avoid any plugin-specific logic outside this file.
53+
- **`providers/class-two-factor-provider.php`** — Abstract base class all providers extend. Defines the required interface: `get_label()`, `is_available_for_user()`, `authentication_page()`, `validate_authentication()`, and optional hooks for REST routes, settings UI, and uninstall cleanup.
54+
- **`providers/`** — Concrete providers: `class-two-factor-totp.php`, `class-two-factor-email.php`, `class-two-factor-backup-codes.php`, `class-two-factor-dummy.php`.
55+
- **`includes/`** — Custom `login_header()` and `login_footer()` template functions that replace the WordPress core versions with additional filter hooks. Excluded from PHPCS because they intentionally deviate from core function signatures. Do not modify files in includes/ directly. They are intentionally kept close to WordPress core function signatures to ease future merging into Core. Any functional changes should go through the filter hooks they expose instead.
56+
- **`tests/`** — PHPUnit tests. See [TESTS.md](TESTS.md).
57+
58+
### Login Flow
59+
60+
1. User submits username/password.
61+
2. `Two_Factor_Core::filter_authenticate()` runs at priority **31** on the `authenticate` filter (one above WP core's 30). If 2FA is required, it intercepts the `WP_User` object to prevent WP from issuing auth cookies.
62+
3. `Two_Factor_Core::wp_login()` runs at priority `PHP_INT_MAX` on `wp_login`, renders the 2FA prompt, and exits.
63+
4. On 2FA form submission, `login_form_validate_2fa` action handles validation and issues the final auth cookie only if the second factor passes.
64+
65+
Auth cookies set during the password phase are tracked via `collect_auth_cookie_tokens` and invalidated before the 2FA step.
66+
67+
### Provider Registration
68+
69+
Providers are registered via the `two_factor_providers` filter, which receives and returns an array of the form:
70+
71+
```php
72+
array( 'Class_Name' => '/absolute/path/to/class-file.php' )
73+
```
74+
75+
The key (class name) is what gets stored in user meta. A per-provider `two_factor_provider_classname_{$provider_key}` filter allows swapping a provider's implementing class without changing its key. Use `two_factor_providers_for_user` to control which providers are available to a specific user.
76+
77+
**The `Two_Factor_Dummy` provider is only available when `WP_DEBUG` is `true`.** It is removed at runtime by `enable_dummy_method_for_debug()` in all other environments. If a dummy provider isn't appearing, check `WP_DEBUG`.
78+
79+
### Provider Self-Registration Pattern
80+
81+
Each concrete provider registers its own hooks in its constructor:
82+
83+
- REST routes → `rest_api_init`
84+
- Assets → `admin_enqueue_scripts`, `wp_enqueue_scripts`
85+
- User profile UI section → `two_factor_user_options_{ClassName}` action
86+
87+
New providers should follow this pattern rather than registering hooks from outside the class.
88+
89+
### Key User Meta (constants on `Two_Factor_Core`)
90+
91+
| Constant | Meta Key | Purpose |
92+
|---|---|---|
93+
| `PROVIDER_USER_META_KEY` | `_two_factor_provider` | Active provider class name |
94+
| `ENABLED_PROVIDERS_USER_META_KEY` | `_two_factor_enabled_providers` | Array of enabled provider class names |
95+
| `USER_META_NONCE_KEY` | `_two_factor_nonce` | Login nonce |
96+
| `USER_RATE_LIMIT_KEY` | `_two_factor_last_login_failure` | Rate limiting timestamp |
97+
| `USER_FAILED_LOGIN_ATTEMPTS_KEY` | `_two_factor_failed_login_attempts` | Failed attempt count |
98+
| `USER_PASSWORD_WAS_RESET_KEY` | `_two_factor_password_was_reset` | Flags compromised-password reset |
99+
100+
### REST API
101+
102+
Namespace: `two-factor/1.0` (constant `Two_Factor_Core::REST_NAMESPACE`). Each provider that exposes REST endpoints registers its own routes in `register_rest_routes()` called from its constructor.
103+
104+
## Code Standards
105+
106+
- PHP 7.2+ compatibility required; enforced by PHPCompatibilityWP.
107+
- Follows WordPress coding standards (WPCS) and WordPress-VIP-Go rules.
108+
- `includes/` is excluded from PHPCS — those files intentionally override core functions.
109+
- The codebase does not fully pass all PHPCS checks (known issue [#437](https://github.com/WordPress/two-factor/issues/437)). Do not treat existing violations as license to introduce new ones.

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@AGENTS.md

TESTS.md

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# Tests
2+
3+
The test suite uses PHPUnit and runs inside the Docker-based `@wordpress/env` environment against a live WordPress install. The `npm run composer` script is a wrapper that executes `composer` inside the `tests-cli` container at the plugin path.
4+
5+
## Running Tests
6+
7+
```bash
8+
# Full test suite
9+
npm test
10+
11+
# Watch mode (re-runs on file changes, no coverage)
12+
npm run test:watch
13+
14+
# Full test suite with coverage (requires xdebug-enabled env)
15+
npm run env start -- --xdebug=coverage
16+
npm test
17+
```
18+
19+
Coverage reports are written to `tests/logs/clover.xml` and `tests/logs/html/`. Open `tests/logs/html/index.html` in a browser to view the HTML report.
20+
21+
### Filtering
22+
23+
Pass PHPUnit arguments through the `composer` wrapper:
24+
25+
```bash
26+
# Run a single test class
27+
npm run composer -- test -- --filter Tests_Two_Factor_Core
28+
29+
# Run a single test method
30+
npm run composer -- test -- --filter test_create_login_nonce
31+
32+
# Run by @group annotation
33+
npm run composer -- test -- --group totp
34+
npm run composer -- test -- --group email
35+
npm run composer -- test -- --group backup-codes
36+
npm run composer -- test -- --group providers
37+
npm run composer -- test -- --group core
38+
39+
# Run a single file
40+
npm run composer -- test -- tests/providers/class-two-factor-totp.php
41+
```
42+
43+
## Test Files
44+
45+
### Plugin Bootstrap — `tests/two-factor.php`
46+
47+
**Class:** `Tests_Two_Factor`
48+
Smoke tests that the plugin loaded correctly: the `TWO_FACTOR_DIR` constant is defined and the core classes exist.
49+
50+
### Core — `tests/class-two-factor-core.php`
51+
52+
**Class:** `Tests_Two_Factor_Core` · **Group:** `core`
53+
The largest test file. Covers the full authentication lifecycle managed by `Two_Factor_Core`:
54+
55+
- Hook registration (`add_hooks`)
56+
- Provider registration and retrieval (`get_providers`, `get_enabled_providers_for_user`, `get_available_providers_for_user`, `get_primary_provider_for_user`)
57+
- Login interception (`filter_authenticate`, `show_two_factor_login`, `process_provider`)
58+
- Login nonce creation, verification, and deletion
59+
- Rate limiting (`get_user_time_delay`, `is_user_rate_limited`)
60+
- Session management: two-factor factored vs. non-factored sessions, session destruction on 2FA enable/disable, revalidation
61+
- Password reset flow (compromise detection, email notifications, reset notices)
62+
- REST API permission callbacks (`rest_api_can_edit_user`)
63+
- User settings actions (`trigger_user_settings_action`, `current_user_can_update_two_factor_options`)
64+
- Uninstall cleanup
65+
- Filter hooks (`two_factor_providers`, `two_factor_primary_provider_for_user`, `two_factor_user_api_login_enable`)
66+
67+
### Provider Base Class — `tests/providers/class-two-factor-provider.php`
68+
69+
**Class:** `Tests_Two_Factor_Provider` · **Group:** `providers`
70+
Tests the abstract `Two_Factor_Provider` base class:
71+
72+
- Singleton pattern (`get_instance`)
73+
- Code generation (`get_code`) and request sanitization (`sanitize_code_from_request`)
74+
- `get_key` returning the class name
75+
- `is_supported_for_user` (globally registered vs. not)
76+
- Default implementations of `get_alternative_provider_label`, `pre_process_authentication`, `uninstall_user_meta_keys`, `uninstall_options`
77+
78+
### TOTP Provider — `tests/providers/class-two-factor-totp.php`
79+
80+
**Class:** `Tests_Two_Factor_Totp` · **Groups:** `providers`, `totp`
81+
Tests `Two_Factor_Totp`:
82+
83+
- Base32 encode/decode (including invalid input exception)
84+
- QR code URL generation
85+
- TOTP key storage and retrieval per user
86+
- Auth code validation (current tick, spaces stripped, invalid chars rejected)
87+
- `validate_code_for_user` replay protection
88+
- Algorithm variants: SHA1, SHA256, SHA512 (code generation and authentication)
89+
- Secret padding (`pad_secret`)
90+
91+
### TOTP REST API — `tests/providers/class-two-factor-totp-rest-api.php`
92+
93+
**Class:** `Tests_Two_Factor_Totp_REST_API` · **Groups:** `providers`, `totp`
94+
Extends `WP_Test_REST_TestCase`. Tests the TOTP REST endpoints:
95+
96+
- Setting a TOTP key with a valid/invalid/missing auth code
97+
- Updating an existing TOTP key
98+
- Deleting own secret
99+
- Admin deleting another user's secret
100+
- Non-admin cannot delete another user's secret
101+
102+
### Email Provider — `tests/providers/class-two-factor-email.php`
103+
104+
**Class:** `Tests_Two_Factor_Email` · **Groups:** `providers`, `email`
105+
Tests `Two_Factor_Email`:
106+
107+
- Token generation and validation (same user, different user, deleted token)
108+
- Email delivery (`generate_and_email_token`)
109+
- Authentication page rendering (no user, no token, existing token)
110+
- `validate_authentication` (valid, missing input, spaces stripped)
111+
- Token TTL and expiry
112+
- Token generation time tracking
113+
- Custom token length filter
114+
- `pre_process_authentication` (resend vs. no resend)
115+
- User options UI output
116+
- Uninstall meta key cleanup
117+
118+
### Backup Codes Provider — `tests/providers/class-two-factor-backup-codes.php`
119+
120+
**Class:** `Tests_Two_Factor_Backup_Codes` · **Groups:** `providers`, `backup-codes`
121+
Tests `Two_Factor_Backup_Codes`:
122+
123+
- Code generation and validation
124+
- Replay prevention (code invalidated after use)
125+
- Cross-user isolation (code invalid for different user)
126+
- `is_available_for_user` (no codes vs. codes generated)
127+
- User options UI output
128+
- Code deletion
129+
- `two_factor_backup_codes_count` filter for customizing code length
130+
131+
### Backup Codes REST API — `tests/providers/class-two-factor-backup-codes-rest-api.php`
132+
133+
**Class:** `Tests_Two_Factor_Backup_Codes_REST_API` · **Groups:** `providers`, `backup-codes`
134+
Extends `WP_Test_REST_TestCase`. Tests the backup codes REST endpoints:
135+
136+
- Generate codes and validate the downloadable file contents
137+
- User cannot generate codes for a different user
138+
- Admin can generate codes for other users
139+
140+
### Dummy Provider — `tests/providers/class-two-factor-dummy.php`
141+
142+
**Class:** `Tests_Two_Factor_Dummy` · **Groups:** `providers`, `dummy`
143+
Tests the `Two_Factor_Dummy` provider (always passes authentication — used as a test fixture):
144+
145+
- `get_instance`, `get_label`, `authentication_page`, `validate_authentication`, `is_available_for_user`
146+
147+
### Dummy Secure Provider — `tests/providers/class-two-factor-dummy-secure.php`
148+
149+
**Class:** `Tests_Two_Factor_Dummy_Secure` · **Groups:** `providers`, `dummy`
150+
Tests `Two_Factor_Dummy_Secure` (a fixture that always _fails_ authentication, used to test the provider class name filter):
151+
152+
- `get_key` override returns `Two_Factor_Dummy`
153+
- Authentication page rendering
154+
- `validate_authentication` always returns false
155+
- `two_factor_provider_classname` filter
156+
157+
## Test Helpers
158+
159+
- **`tests/bootstrap.php`** — Locates the WordPress test library (via `WP_TESTS_DIR` env var, relative path, or `/tmp/wordpress-tests-lib`), loads the plugin via `muplugins_loaded`, then boots the WP test environment.
160+
- **`tests/class-secure-dummy.php`** — Defines `Two_Factor_Dummy_Secure`, a test-only provider class that spoofs the key of `Two_Factor_Dummy` but always fails `validate_authentication`. Used by `Tests_Two_Factor_Dummy_Secure` and some core tests.

0 commit comments

Comments
 (0)