Skip to content

Commit

Permalink
Make CAPTCHAs more configurable, document the settings
Browse files Browse the repository at this point in the history
  • Loading branch information
CatoTH committed Nov 23, 2024
1 parent 39c3b6b commit 538c1e9
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 11 deletions.
1 change: 1 addition & 0 deletions History.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
- When merging amendments into a motion, the default setting now is to create a resolution, not a new motion.
- Security improvement: When logging in, and a new verion of PHP (like 8.4) suggests a stronger default password hashing, the stored hash is updated accordingly.
- A new translation is provided: Montenegrin (thanks to Danilo Boskovic)
- Administrators of an installation can modify the behavior of the CAPTCHAs on registration (see README).
- Some compatibility issues with PHP 8.4 were resolved.
- Bugfix: Tabular data was not encoded correctly in the PHP-based PDF export.
- Bugfix: The PDF with all amendments embedded into the motion text could not be generated if a Weasyprint-based PDF layout was selected.
Expand Down
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,39 @@ Note that this might in some edge case lead to old information being shown and i

As a rule of thumb, this setting should be considered if you expect close to 1.000 motions and amendments or more in one consultation.

### Securing Accounts

Antragsgrün comes with built-in support for protecting user accounts from brute-force accounts. By default:
- A CAPTCHA needs to be solved after three unsuccessful login attempts for every further login attempt.
- Users can opt in to protect their accounts using a second factor authentication app (TOTP).

#### Configuring the CAPTCHA

The default behavior of the CAPTCHA can be modified in the `config.json`:
- The `mode` indicates when a CAPTCHA is shown. The default `throttle` requires it after three unsuccessful attempts, balancing security with trying not to bother users too much. `always` always requires entering a CAPTCHA, `never` disables it entirely.
- `difficulty` defaults to `normal`, which should be solvable by most users. To make it easier (no distortion of image), set it to `easy`.
- `ignoredIps` is a list of IP addresses that will never receive a CAPTCHA. This is often necessary on conventions where all delegates are sharing one WiFi IP address and unsuccessful login attempts of one delegate would otherwise trigger CAPTCHA-behavior for all others.

```json
{
"captcha": {
"mode": "always", // Options: "never", "throttle", "always"
"ignoredIps": [
"127.0.0.1",
"::1"
],
"difficulty": "easy" // Options: "easy", "normal"
}
}
```

#### Configuring / Enforcing 2FA

By default, users have the option to secure their account with a TOTP-based second factor (supported by many apps like Authy, Google Authenticator, FreeOTP or password managers). Super-Admins can change this behavior on an *per-user-basis*:
- Setting a second factor can be enforced.
- Setting a second factor can be disabled (changing passwords can be prevented too, e.g. for accounts meant to be shared).
- A second factor can be removed, e.g. if the user lost access to their 2FA-app.

### JWT Key Signing

Some of the more advanced features of Antragsgrün need JWT signing set up. Right now, this is only the integration of the Live Server, but in the future this will also enable logged in access to the REST API.
Expand Down
12 changes: 8 additions & 4 deletions components/Captcha.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,21 @@ class Captcha
{
public static function needsCaptcha(?string $username): bool
{
if (FailedLoginAttempt::needsLoginThrottling($username)) {
return true;
}
return AntragsgruenApp::getInstance()->loginCaptcha;
return match (AntragsgruenApp::getInstance()->captcha['mode']) {
AntragsgruenApp::CAPTCHA_MODE_ALWAYS => true,
AntragsgruenApp::CAPTCHA_MODE_NEVER => false,
default => FailedLoginAttempt::needsLoginThrottling($username),
};
}

public static function createInlineCaptcha(): string
{
$builder = new CaptchaBuilder();
$builder->distort = true;
$builder->bgColor = '#fff';
if (AntragsgruenApp::getInstance()->captcha['difficulty'] === AntragsgruenApp::CAPTCHA_DIFFICULTY_EASY) {

Check failure on line 27 in components/Captcha.php

View workflow job for this annotation

GitHub Actions / evaluate-pr

Strict comparison using === between int and 'easy' will always evaluate to false.
$builder->applyEffects = false;
}
$builder->build(300, 80);

$phrase = $builder->phrase;
Expand Down
9 changes: 6 additions & 3 deletions config/config_tests.template.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@
"baseLanguage": "de",
"multisiteMode": true,
"confirmEmailAddresses": true,
"loginCaptchaIgnoredIps": [
"127.0.0.1"
],
"captcha": {
"ignoredIps": [
"127.0.0.1",
"::1"
]
},
"domainPlain": "http://localhost:8080/index-test.php",
"domainSubdomain": "http://localhost:8080/index-test.php",
"prependWWWToSubdomain": false,
Expand Down
2 changes: 1 addition & 1 deletion models/db/FailedLoginAttempt.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public static function logAttempt(string $username): void

private static function needsLoginThrottlingByIp(): bool
{
$ignoredIps = AntragsgruenApp::getInstance()->loginCaptchaIgnoredIps;
$ignoredIps = AntragsgruenApp::getInstance()->captcha['ignoredIps'];
if (in_array(self::getCurrentIp(), $ignoredIps)) {
return false;
}
Expand Down
40 changes: 37 additions & 3 deletions models/settings/AntragsgruenApp.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ class AntragsgruenApp implements \JsonSerializable
{
use JsonConfigTrait;

public const CAPTCHA_MODE_NEVER = 'never';
public const CAPTCHA_MODE_THROTTLE = 'throttle';
public const CAPTCHA_MODE_ALWAYS = 'always';

public const CAPTCHA_DIFFICULTY_EASY = 'easy';
public const CAPTCHA_DIFFICULTY_MEDIUM = 'medium';
public const CAPTCHA_DIFFICULTY_HARD = 'hard';

public ?array $dbConnection = null;
public ?string $siteSubdomain = null;
public ?array $redis = null;
Expand Down Expand Up @@ -37,9 +45,6 @@ class AntragsgruenApp implements \JsonSerializable
/** @var string[] */
public array $blockedSubdomains = ['www', 'rest', 'ftp', 'smtp', 'imap'];
public int $autoLoginDuration = 31536000; // 1 Year
public bool $loginCaptcha = false; // Forces captcha even at the first login attempt
/** @var string[] */
public array $loginCaptchaIgnoredIps = [];
public ?string $xelatexPath = null; // @TODO OBSOLETE
public ?string $xdvipdfmx = null; // @TODO OBSOLETE
public ?string $lualatexPath = null;
Expand All @@ -55,6 +60,13 @@ class AntragsgruenApp implements \JsonSerializable
public ?string $updateKey = null;
public ?string $jwtPrivateKey = null;

/** @var array{mode: string, ignoredIps: string[], difficulty: int} */
public array $captcha = [

Check failure on line 64 in models/settings/AntragsgruenApp.php

View workflow job for this annotation

GitHub Actions / evaluate-pr

Property app\models\settings\AntragsgruenApp::$captcha (array{mode: string, ignoredIps: array<string>, difficulty: int}) does not accept default value of type array{mode: 'throttle', ignoredIps: array{}, difficulty: 'medium'}.
'mode' => self::CAPTCHA_MODE_THROTTLE,
'ignoredIps' => [],
'difficulty' => self::CAPTCHA_DIFFICULTY_MEDIUM,
];

/** @var array<class-string<ModuleBase>> */
protected array $plugins = [];

Expand Down Expand Up @@ -96,6 +108,28 @@ public function __construct($data)
}
}

public function setCaptcha(?array $captcha): void
{
if (!is_array($captcha)) {
return;
}
if (isset($captcha['mode'])) {
if (!in_array($captcha['mode'], [self::CAPTCHA_MODE_NEVER, self::CAPTCHA_MODE_THROTTLE, self::CAPTCHA_MODE_ALWAYS], true)) {
throw new \Exception('Invalid captcha mode setting');
}
$this->captcha['mode'] = $captcha['mode'];
}
if (isset($captcha['difficulty'])) {
if (!in_array($captcha['difficulty'], [self::CAPTCHA_DIFFICULTY_EASY, self::CAPTCHA_DIFFICULTY_MEDIUM, self::CAPTCHA_DIFFICULTY_HARD], true)) {
throw new \Exception('Invalid captcha difficulty setting');
}
$this->captcha['difficulty'] = $captcha['difficulty'];

Check failure on line 126 in models/settings/AntragsgruenApp.php

View workflow job for this annotation

GitHub Actions / evaluate-pr

Property app\models\settings\AntragsgruenApp::$captcha (array{mode: string, ignoredIps: array<string>, difficulty: int}) does not accept array{mode: string, ignoredIps: array<string>, difficulty: 'easy'|'hard'|'medium'}.
}
if (isset($captcha['ignoredIps']) && is_array($captcha['ignoredIps'])) {
$this->captcha['ignoredIps'] = $captcha['ignoredIps'];
}
}

/**
* @throws \yii\db\Exception
*/
Expand Down

0 comments on commit 538c1e9

Please sign in to comment.