summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/users.py9
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/auth-page.js9
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/keyderive.js6
-rw-r--r--packages/meshbay-hub/tests/test_register_captcha.py58
4 files changed, 78 insertions, 4 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/users.py b/packages/meshbay-hub/src/meshbay_hub/api/users.py
index 559cfa6..9b70b59 100644
--- a/packages/meshbay-hub/src/meshbay_hub/api/users.py
+++ b/packages/meshbay-hub/src/meshbay_hub/api/users.py
@@ -150,8 +150,13 @@ async def register(
return {"user_id": found.id, "email_verification_required": True}
raise HTTPException(status_code=409, detail="Username already taken")
- # Captcha gate — web path only (native clients send auth_key)
- if _cfg and _cfg.captcha.enabled and not body.auth_key:
+ # Captcha gate — every fresh registration when a captcha is configured, with
+ # no client carve-out. The earlier `and not body.auth_key` exempted anything
+ # that sent an `auth_key`, which is *every* real client (the browser sends it
+ # too, from the password split) — so the check was off for everyone, and a
+ # bot skipped it by sending the field. The desktop client is Chromium and
+ # renders the same widget, so it has no need of an exemption either.
+ if _cfg and _cfg.captcha.enabled:
await _verify_captcha_or_raise(body.captcha_token, request)
# Email uniqueness (only active or pending accounts)
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js b/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js
index df08bc4..4c00137 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js
@@ -237,8 +237,11 @@ export function RegisterPage() {
const rk = window.MeshBayKeys.generateRecoveryKey();
// `name` (trimmed), not the raw field: the hub stores the trimmed
// username and every key derivation must fold in the same string.
+ // `captcha.token` rides along — the submit button is already disabled
+ // until it is set when a captcha is configured (see the form below).
await window.MeshBayKeys.registerUser(
- name, email, password, emailRecovery ? rk.mnemonic : null);
+ name, email, password, emailRecovery ? rk.mnemonic : null,
+ captcha.token);
setRecoveryMnemonic(rk.mnemonic);
session.recoveryKey =
await window.MeshBayKeys.deriveRecoveryKey(rk.mnemonic, name);
@@ -257,6 +260,10 @@ export function RegisterPage() {
}
} catch (err) {
setError(err.message);
+ // A reCAPTCHA token is single-use: after a failed attempt (name taken,
+ // e-mail in use…) it is spent, so clear it and make the user solve a
+ // fresh one before the next try. No-op when no captcha is configured.
+ captcha.reset();
} finally {
setLoading(false);
}
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/keyderive.js b/packages/meshbay-hub/src/meshbay_hub/static/keyderive.js
index 0aaa6a5..a540a94 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/keyderive.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/keyderive.js
@@ -276,7 +276,7 @@ async function decryptBundle(bundleB64, password, username) {
*
* Returns the raw private keys for immediate use after registration.
*/
-async function registerUser(username, email, password, recoveryMnemonic) {
+async function registerUser(username, email, password, recoveryMnemonic, captchaToken) {
// No keypair here any more. Identity keys are per node: one is generated the
// first time this account joins a given node, encrypted under the passphrase,
// and left with that node. So an operator who cracks what sits on their own
@@ -291,6 +291,10 @@ async function registerUser(username, email, password, recoveryMnemonic) {
// appends it to the verification e-mail and stores it nowhere
// (docs/auth-confirm.md §4.4). Omitted when they chose to save it themselves.
if (recoveryMnemonic) payload.recovery_key = recoveryMnemonic;
+ // reCAPTCHA response, when the hub has a captcha configured. The widget lives
+ // in RegisterPage (auth-page.js); this function just forwards its token. A
+ // hub with no captcha configured sends nothing and the server does not check.
+ if (captchaToken) payload.captcha_token = captchaToken;
const resp = await hubCall('/v1/users/register', {
method: 'POST',
diff --git a/packages/meshbay-hub/tests/test_register_captcha.py b/packages/meshbay-hub/tests/test_register_captcha.py
new file mode 100644
index 0000000..befc1e2
--- /dev/null
+++ b/packages/meshbay-hub/tests/test_register_captcha.py
@@ -0,0 +1,58 @@
+"""Registration CAPTCHA is enforced for every fresh account when configured.
+
+The gate used to be skipped whenever the request carried an `auth_key` — which
+every real client sends (the password split) — so it protected nobody and a bot
+skipped it by including the field. It now runs on `captcha.enabled` alone; the
+desktop client is Chromium and renders the same widget.
+"""
+
+import pytest
+
+
+@pytest.fixture
+def captcha_on(client, monkeypatch):
+ """Turn on a fake captcha: any config with both keys is `enabled`, and
+ verification succeeds only for the token 'good-token'."""
+ from meshbay_hub.api.users import _cfg
+ monkeypatch.setattr(_cfg.captcha, "site_key", "test-site")
+ monkeypatch.setattr(_cfg.captcha, "secret_key", "test-secret")
+
+ async def fake_verify(secret, token, remote_ip=None):
+ return token == "good-token"
+
+ monkeypatch.setattr("meshbay_hub.captcha.verify_captcha", fake_verify)
+
+
+def _body(**over):
+ b = {"username": "newbie", "email": "newbie@t.com", "auth_key": "a" * 44}
+ b.update(over)
+ return b
+
+
+@pytest.mark.asyncio
+async def test_missing_captcha_rejected_even_with_auth_key(client, captcha_on):
+ r = await client.post("/v1/users/register", json=_body())
+ assert r.status_code == 400
+ assert r.json()["detail"] == "captcha_required"
+
+
+@pytest.mark.asyncio
+async def test_bad_captcha_rejected(client, captcha_on):
+ r = await client.post("/v1/users/register",
+ json=_body(captcha_token="wrong"))
+ assert r.status_code == 400
+ assert r.json()["detail"] == "captcha_failed"
+
+
+@pytest.mark.asyncio
+async def test_good_captcha_accepted(client, captcha_on):
+ r = await client.post("/v1/users/register",
+ json=_body(captcha_token="good-token"))
+ assert r.status_code == 201
+
+
+@pytest.mark.asyncio
+async def test_no_captcha_configured_still_registers(client):
+ # Default test config has no captcha keys — registration proceeds without one.
+ r = await client.post("/v1/users/register", json=_body())
+ assert r.status_code == 201