Skip to content

Captcha

Included free on every install, with one captcha provider for the instance. A provider and keys of a tenant's own need a license with the multitenant-customization feature. See pricing.

Captcha slows down password guessing on the admin sign-in. After a few failed attempts, the next attempt must carry a token from Cloudflare Turnstile, hCaptcha or Google reCAPTCHA v3, and the token is checked with the provider before the password is. A successful sign-in resets the count.

Failed attempts on POST /api/admin/auth/login are counted three ways, and any one of them can trigger the challenge:

  • one address trying one email
  • one address trying many emails
  • one email tried from many addresses

Emails are compared without case or surrounding spaces. Once a count passes CAPTCHA_THRESHOLD, a failed sign-in answers 401 with three extra fields:

{ "error": "Invalid email or password.", "code": "AUTH_INVALID_CREDENTIALS", "captcha_required": true, "captcha_site_key": "0x4AAAAAAA...", "captcha_provider": "turnstile", "request_id": "..." }

captcha_provider names the widget that captcha_site_key belongs to. The admin console's sign-in page shows that widget, keeps the email address you typed, holds Sign in until the widget has issued a token, and shows a fresh widget after each refused attempt.

The counts are kept for the whole instance and for each tenant. A failed admin sign-in that arrives on a tenant's own domain counts in both, and the admin sign-in is challenged on the instance count alone, because it looks accounts up across every tenant. A tenant's own forms use the captcha.attempt flow node, which checks, records a failure or clears the count for the caller on the tenant's own count and answers required and threshold. A failure there also counts for the instance. With multitenant-customization, a tenant can save its own failure_threshold, from 1 to 1000, with its captcha settings. Without one, the tenant's count is read against CAPTCHA_THRESHOLD.

Captcha runs on every install and does nothing until both keys are set:

Terminal window
CAPTCHA_PROVIDER=turnstile
CAPTCHA_SITE_KEY=<site key from the provider>
CAPTCHA_SECRET_KEY=<secret key from the provider>

With either key missing, no attempt is counted and no sign-in is challenged. A change to these settings applies without a restart.

This needs a site key and a secret key from your provider.

  1. Set the three variables above and sign in with a wrong password three times:

    Terminal window
    curl -X POST http://localhost:3001/api/admin/auth/login \
    -H "Content-Type: application/json" \
    -d '{"email": "you@example.com", "password": "wrong-password"}'

    Once the failures reach the threshold, the answer is the 401 above, with "captcha_required": true.

  2. Try again without a token:

    Terminal window
    curl -X POST http://localhost:3001/api/admin/auth/login \
    -H "Content-Type: application/json" \
    -d '{"email": "you@example.com", "password": "<password>"}'
    { "code": "captcha_verification_failed", "captcha_required": true, "captcha_site_key": "0x4AAAAAAA...", "captcha_provider": "turnstile" }

    The status is 400.

  3. Show the widget with the site key, then send its token with the attempt:

    Terminal window
    curl -X POST http://localhost:3001/api/admin/auth/login \
    -H "Content-Type: application/json" \
    -d '{"email": "you@example.com", "password": "<password>", "captcha_token": "<token from the widget>"}'

    A correct password now signs in and resets the counts.

  4. Open the admin console's sign-in page and fail three times. The widget appears above Sign in.

The captcha.verify node checks a token with the configured provider, the same way the sign-in does. Use it on a public form handled by a flow.

SettingRequiredMeaning
tokenyesThe widget's token, such as {{ trigger.body.captcha_token }}.
ipnoThe client address, passed to the provider.
actionnoThe action the token must carry, for providers that label tokens.

The node's out has ok, provider, score (null when the provider sends none), action and error_codes. A refused token does not fail the step: ok is false and error_codes says why, so the flow can branch on it. The step fails when no secret key is set, neither the tenant's nor the instance's.

With multitenant-customization, a tenant admin saves a provider and keys for the tenant's own surfaces: the captcha.verify node in its flows, such as a sign-up form, and the widget on its own pages.

Terminal window
curl -X PUT http://localhost:3001/api/admin/captcha/settings \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"provider": "hcaptcha", "site_key": "<site key>", "secret_key": "<secret key>"}'
FieldMeaning
providerturnstile, hcaptcha or recaptcha.
site_keyThe public key the widget shows.
secret_keyThe private key tokens are checked with. Never returned. Leave it empty on a later save to keep the stored one.
score_floorFor reCAPTCHA only, the lowest score accepted, from 0 to 1.

The answer, like GET on the same path, carries the tenant's settings with has_secret in place of the secret, the instance's public settings, source (tenant or instance, which settings the tenant's flows verify with) and licensed. DELETE clears the tenant's settings, so its flows use the instance's again. Storing a secret needs ENCRYPTION_KEY or the JWT secret.

A page that shows the widget before anyone signs in reads GET /api/admin/captcha/site-key, which needs no credential. Its top-level provider, site_key and enabled are the instance settings. Its tenant carries the same three for the tenant the request reached, when that tenant saved its own, and is null otherwise. No secret is ever in it.

If the license lapses, the saved settings keep verifying. A new or changed save answers 402, and resending what is stored and clearing stay free.

VariableWhat it doesDefault
CAPTCHA_PROVIDERturnstile, hcaptcha or recaptcha.turnstile
CAPTCHA_SITE_KEYThe public key the sign-in page shows the widget with. It is returned to clients.empty
CAPTCHA_SECRET_KEYThe private key used to check tokens. It is never returned.empty
CAPTCHA_THRESHOLDFailed attempts before a challenge. Raise it when many people share one address, such as behind an office NAT.3
TRUSTED_PROXIESComma-separated CIDRs of your reverse proxies. Only these may set X-Forwarded-For. Without it, the connecting address is used.empty

If you set LYEVE_PLUGINS, include captcha in it.

MethodPathWhoResult
GET/api/admin/captcha/settingsAdminThe tenant's settings, the instance's public settings, source and licensed.
PUT/api/admin/captcha/settingsAdminSave the tenant's provider and keys. Needs multitenant-customization.
DELETE/api/admin/captcha/settingsAdminClear the tenant's settings. 204.
GET/api/admin/captcha/site-keyAnyoneThe public keys a page needs to show the widget.
StatusMessageCause
400invalid request bodyThe body is not JSON.
402payment_required, naming feature:multitenant_customizationA new or changed save without the license.
422secret_key is required, or a message naming the fieldThe settings are not valid, or the instance has no key to encrypt the secret.