{"id":49492413,"url":"https://github.com/radishconcepts/radish-wp-2fa","last_synced_at":"2026-05-01T07:05:04.033Z","repository":{"id":354511248,"uuid":"1223915146","full_name":"radishconcepts/radish-wp-2fa","owner":"radishconcepts","description":"Enforceable, frontend-first two-factor authentication for WordPress with role-based hard enforcement.","archived":false,"fork":false,"pushed_at":"2026-04-28T20:52:42.000Z","size":69,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-28T22:28:09.236Z","etag":null,"topics":["2fa","multisite","security","totp","two-factor-authentication","wordpress","wordpress-plugin"],"latest_commit_sha":null,"homepage":null,"language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/radishconcepts.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":".github/CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":".github/CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":".github/SECURITY.md","support":".github/SUPPORT.md","governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":null},"created_at":"2026-04-28T19:27:05.000Z","updated_at":"2026-04-28T20:52:47.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/radishconcepts/radish-wp-2fa","commit_stats":null,"previous_names":["radishconcepts/radish-wp-2fa"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/radishconcepts/radish-wp-2fa","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/radishconcepts%2Fradish-wp-2fa","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/radishconcepts%2Fradish-wp-2fa/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/radishconcepts%2Fradish-wp-2fa/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/radishconcepts%2Fradish-wp-2fa/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/radishconcepts","download_url":"https://codeload.github.com/radishconcepts/radish-wp-2fa/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/radishconcepts%2Fradish-wp-2fa/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32487746,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-30T13:12:12.517Z","status":"online","status_checked_at":"2026-05-01T02:00:05.856Z","response_time":64,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["2fa","multisite","security","totp","two-factor-authentication","wordpress","wordpress-plugin"],"created_at":"2026-05-01T07:04:59.354Z","updated_at":"2026-05-01T07:05:04.020Z","avatar_url":"https://github.com/radishconcepts.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Radish 2FA\n\n[![Tests](https://github.com/radishconcepts/radish-wp-2fa/actions/workflows/tests.yml/badge.svg)](https://github.com/radishconcepts/radish-wp-2fa/actions/workflows/tests.yml)\n[![License: GPL v2+](https://img.shields.io/badge/License-GPL_v2+-blue.svg)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.html)\n[![PHP Version](https://img.shields.io/badge/php-%5E8.1-8892BF.svg)](https://www.php.net/)\n\nTwo-factor authentication (TOTP) for WordPress with **hard role-based enforcement** and a friendly frontend setup flow for clients — no QR codes hidden on the profile page, no instructions that confuse editors.\n\n## Features\n\n- **Settings page** to choose which WP roles must use 2FA (network admin on multisite, otherwise Settings → Radish 2FA).\n- **Frontend setup flow** at `/2fa/setup` and `/2fa/challenge` instead of a wp-admin profile page. Theme overrides via `your-theme/radish-2fa/{template}.php`.\n- **Truly mandatory**: without completed 2FA no auth cookie is issued, and existing sessions are terminated as soon as you add a role to enforcement. There is no \"skip\" option.\n- **Multisite-ready**: settings and user meta are network-wide.\n- **Backup codes**: 10 codes, shown once at setup, bcrypt-hashed at rest.\n- **TOTP secret encrypted at rest** with `sodium_crypto_secretbox`; the key is derived from `AUTH_KEY + SECURE_AUTH_KEY`.\n- **API protection**: REST/XML-RPC password logins are blocked for 2FA users; Application Passwords keep working.\n- **Lockout recovery**: constant in `wp-config.php`, WP-CLI command, or via wp-admin.\n\n## Installation\n\n### Option 1 — Composer (recommended)\n\nIn your site's `composer.json`:\n\n```json\n{\n    \"require\": {\n        \"radishconcepts/radish-wp-2fa\": \"^0.1\",\n        \"composer/installers\": \"^2.0\"\n    },\n    \"repositories\": [\n        { \"type\": \"vcs\", \"url\": \"https://github.com/radishconcepts/radish-wp-2fa\" }\n    ],\n    \"extra\": {\n        \"installer-paths\": {\n            \"wp-content/plugins/{$name}/\": [ \"type:wordpress-plugin\" ]\n        }\n    }\n}\n```\n\n```bash\ncomposer require radishconcepts/radish-wp-2fa\n```\n\nThe plugin installs into `wp-content/plugins/radish-2fa/` (the `installer-name` in the plugin's `composer.json` ensures the directory is named `radish-2fa`, not `radish-wp-2fa`).\n\n### Option 2 — Manual installation\n\n```bash\ngit clone https://github.com/radishconcepts/radish-wp-2fa.git wp-content/plugins/radish-2fa\ncd wp-content/plugins/radish-2fa\ncomposer install --no-dev --optimize-autoloader\n```\n\n### Activation\n\nActivate the plugin via WordPress (network-activate on multisite).\n\n**Important**: deactivate any other 2FA plugins (Two Factor, miniOrange, etc.) — their login hooks will conflict.\n\n## Configuration\n\n1. Go to **Network Settings → Radish 2FA** (multisite) or **Settings → Radish 2FA** (single site).\n2. Tick the roles that must use 2FA.\n3. On multisite: optionally enable \"Require 2FA for all super admins\" (strongly recommended).\n4. Click **Save**.\n\nWhen you save, active sessions for users newly falling under enforcement are terminated immediately — so they get caught by the enforcement flow on their next request.\n\n## How it works\n\n```\n       wp-login.php (username + password)\n                   │\n                   ▼\n   Does their role require 2FA? ───── no ────▶  Normal login\n                   │\n                  yes\n                   │\n                   ▼\n   Auth cookie is suppressed (no session)\n                   │\n        ┌──────────┴──────────┐\n        │                     │\n   no TOTP yet?           already enrolled?\n        │                     │\n        ▼                     ▼\n  /2fa/setup             /2fa/challenge\n  (QR + verify           (6-digit OR\n   + backup codes)        backup code)\n        │                     │\n        └──────────┬──────────┘\n                   ▼\n         wp_set_auth_cookie()\n         redirect to redirect_to\n```\n\nTokens live for 5 minutes, are single-use, and are stored in `site_transient` under `sha256(token)` so a database dump never reveals the token itself.\n\n## Lockout recovery\n\nThree ways to get someone out of a 2FA deadlock.\n\n### 1. `wp-config.php` constant\n\n```php\n// Single user\ndefine( 'RADISH_2FA_DISABLE_FOR_USER_ID', 1 );\n\n// Multiple users\ndefine( 'RADISH_2FA_DISABLE_FOR_USER_ID', [ 1, 2, 5 ] );\n```\n\nFor users in this list, the plugin skips **all** enforcement — they log in without 2FA. Only use this for emergencies or service accounts. Remove as soon as it's no longer needed.\n\n### 2. WP-CLI\n\n```bash\n# Show the current status of a user\nwp radish-2fa status arjan\nwp radish-2fa status arjan@example.com\nwp radish-2fa status 5\n\n# Reset 2FA for a user (clears secret + backup codes, terminates sessions)\nwp radish-2fa disable arjan\n```\n\n`\u003cuser\u003e` accepts a user ID, login, or email. On multisite, add `--url=…` or `--network` where relevant.\n\n### 3. Via wp-admin (super admin)\n\nGo to the **user-edit page** of the affected user (`Users → All Users → Edit`). At the bottom there's a **Two-factor authentication** block showing the current status (last enrolled, backup codes remaining, last used). Click **Reset two-factor authentication** and confirm — the secret and backup codes are wiped and all sessions for that user are terminated.\n\nOn multisite `is_super_admin()` is required; on single site the `edit_users` capability is enough.\n\n## Overriding templates\n\nPlace your own version of a template in your theme:\n\n```\nyour-theme/radish-2fa/setup.php\nyour-theme/radish-2fa/challenge.php\nyour-theme/radish-2fa/backup-codes.php\nyour-theme/radish-2fa/expired.php\n```\n\nThe plugin checks the theme version first via `locate_template()`. Use the files in `radish-2fa/templates/` as a starting point.\n\n## Translation\n\nThe source code is English. A Dutch translation ships in `languages/radish-2fa-nl_NL.po` (plus the compiled `.mo`). For other languages:\n\n```bash\n# Add a new language\ncp languages/radish-2fa.pot languages/radish-2fa-de_DE.po\n# … translate via Poedit, then:\nmsgfmt -o languages/radish-2fa-de_DE.mo languages/radish-2fa-de_DE.po\n```\n\nWordPress loads the correct `.mo` automatically based on the site locale.\n\n## Hooks\n\n| Hook | Type | Args | Purpose |\n|------|------|------|---------|\n| `radish_2fa_totp_issuer` | filter | `(string) $issuer` | Customize the issuer name shown in the TOTP app (default: site name). |\n\n## Tests\n\n```bash\ncomposer install\ncomposer test\n```\n\nUnit tests (PHPUnit 10) cover Crypto, Totp, BackupCodes, Nonce, Routes, and Roles. Lightweight WordPress function stubs in `tests/bootstrap.php` — no full WordPress test suite required.\n\n## Requirements\n\n- PHP **8.1+** (libsodium is built in from 7.2)\n- WordPress **6.2+**\n- `pragmarx/google2fa` ^8.0\n- `bacon/bacon-qr-code` ^2.0 || ^3.0\n\n## Troubleshooting\n\n**\"This link has expired\" after logging in** — The token is older than 5 minutes or already used. Log in again.\n\n**404 on `/2fa/setup`** — Rewrite rules haven't been flushed. Visit the homepage of that (sub)site once (auto-flush via version check), or go to Settings → Permalinks → Save.\n\n**A user is locked out** — See [Lockout recovery](#lockout-recovery).\n\n**Two Factor / miniOrange interferes** — Deactivate both before using Radish 2FA. Their `wp_login` hooks conflict.\n\n**An API script no longer works** — 2FA users can no longer authenticate against REST/XML-RPC with their password. Generate an **Application Password** (Users → Profile → Application Passwords) and use that instead of the account password.\n\n## Security model\n\n- **TOTP secrets** are encrypted at rest (sodium secretbox, key derived from `AUTH_KEY + SECURE_AUTH_KEY` via HKDF-SHA256). A database leak alone is not enough to recover secrets.\n- **Backup codes** are bcrypt-hashed via `wp_hash_password`. Worst-case verify time for an incorrect code is ~800ms (10 hashes × ~80ms) — a natural rate limit.\n- **Login nonce**: 128 bits of entropy (`bin2hex(random_bytes(16))`), 5-minute TTL, stored under `sha256(token)`, single-use.\n- **Auth cookie suspension**: during the password step `send_auth_cookies → __return_false`; the session token is grabbed from the cookie stream via the `auth_cookie` filter and destroyed immediately. No cookie ever leaves the server before 2FA is complete.\n- **`\u003cmeta name=\"referrer\" content=\"no-referrer\"\u003e`** on every 2FA page prevents token leaks via the Referer header.\n\n## Contributing\n\nSee [CONTRIBUTING.md](.github/CONTRIBUTING.md) for development setup, coding standards, and the pull request workflow. Security issues should be reported privately — see [SECURITY.md](.github/SECURITY.md).\n\n## License\n\nGPL-2.0-or-later. See [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fradishconcepts%2Fradish-wp-2fa","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fradishconcepts%2Fradish-wp-2fa","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fradishconcepts%2Fradish-wp-2fa/lists"}