Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(TV-57): implement altcha captcha #931

Open
wants to merge 12 commits into
base: next
Choose a base branch
from
6 changes: 3 additions & 3 deletions zmscitizenapi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,10 @@ sequenceDiagram
| FRIENDLY_CAPTCHA_SITE_KEY | FriendlyCaptcha site key | "" |
| FRIENDLY_CAPTCHA_ENDPOINT | FriendlyCaptcha verification endpoint | https://eu-api.friendlycaptcha.eu/api/v1/siteverify |
| FRIENDLY_CAPTCHA_ENDPOINT_PUZZLE | FriendlyCaptcha puzzle endpoint | https://eu-api.friendlycaptcha.eu/api/v1/puzzle |
| ALTCHA_CAPTCHA_SECRET_KEY | Altcha secret key | "" |
| ALTCHA_CAPTCHA_SITE_KEY | Altcha site key | "" |
| ALTCHA_CAPTCHA_ENDPOINT | Altcha verification endpoint | https://eu.altcha.org/form/ |
| ALTCHA_CAPTCHA_ENDPOINT_PUZZLE | Altcha puzzle endpoint | https://eu.altcha.org/ |
| ALTCHA_CAPTCHA_SITE_SECRET | Altcha site secret | "" |
| ALTCHA_CAPTCHA_ENDPOINT_CHALLENGE | Altcha challenge endpoint | https://captcha-k.muenchen.de/api/v1/captcha/challenge |
| ALTCHA_CAPTCHA_ENDPOINT_VERIFY | Altcha verification endpoint | https://captcha-k.muenchen.de/api/v1/captcha/verify |
| **Rate Limiting** |
| RATE_LIMIT_MAX_REQUESTS | Maximum requests per window | 60 |
| RATE_LIMIT_CACHE_TTL | Rate limit cache TTL in seconds | 60 |
Expand Down
16 changes: 8 additions & 8 deletions zmscitizenapi/src/Zmscitizenapi/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ class Application extends \BO\Slim\Application
public static string $FRIENDLY_CAPTCHA_SITE_KEY;
public static string $FRIENDLY_CAPTCHA_ENDPOINT;
public static string $FRIENDLY_CAPTCHA_ENDPOINT_PUZZLE;
public static string $ALTCHA_CAPTCHA_SECRET_KEY;
public static string $ALTCHA_CAPTCHA_SITE_KEY;
public static string $ALTCHA_CAPTCHA_ENDPOINT;
public static string $ALTCHA_CAPTCHA_ENDPOINT_PUZZLE;
public static string $ALTCHA_CAPTCHA_SITE_SECRET;
public static string $ALTCHA_CAPTCHA_ENDPOINT_CHALLENGE;
public static string $ALTCHA_CAPTCHA_ENDPOINT_VERIFY;
// Rate limiting config
public static int $RATE_LIMIT_MAX_REQUESTS;
public static int $RATE_LIMIT_CACHE_TTL;
Expand Down Expand Up @@ -108,12 +108,12 @@ private static function initializeCaptcha(): void
?: 'https://eu-api.friendlycaptcha.eu/api/v1/siteverify';
self::$FRIENDLY_CAPTCHA_ENDPOINT_PUZZLE = getenv('FRIENDLY_CAPTCHA_ENDPOINT_PUZZLE')
?: 'https://eu-api.friendlycaptcha.eu/api/v1/puzzle';
self::$ALTCHA_CAPTCHA_SECRET_KEY = getenv('ALTCHA_CAPTCHA_SECRET_KEY') ?: '';
self::$ALTCHA_CAPTCHA_SITE_KEY = getenv('ALTCHA_CAPTCHA_SITE_KEY') ?: '';
self::$ALTCHA_CAPTCHA_ENDPOINT = getenv('ALTCHA_CAPTCHA_ENDPOINT')
?: 'https://eu.altcha.org/form/';
self::$ALTCHA_CAPTCHA_ENDPOINT_PUZZLE = getenv('ALTCHA_CAPTCHA_ENDPOINT_PUZZLE')
?: 'https://eu.altcha.org/';
self::$ALTCHA_CAPTCHA_SITE_SECRET = getenv('ALTCHA_CAPTCHA_SITE_SECRET') ?: '';
self::$ALTCHA_CAPTCHA_ENDPOINT_CHALLENGE = getenv('ALTCHA_CAPTCHA_ENDPOINT_CHALLENGE')
?: 'https://captcha-k.muenchen.de/api/v1/captcha/challenge';
self::$ALTCHA_CAPTCHA_ENDPOINT_VERIFY = getenv('ALTCHA_CAPTCHA_ENDPOINT_VERIFY')
?: 'https://captcha-k.muenchen.de/api/v1/captcha/verify';
}

private static function initializeCache(): void
Expand Down
35 changes: 18 additions & 17 deletions zmscitizenapi/src/Zmscitizenapi/Models/Captcha/AltchaCaptcha.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,21 @@ class AltchaCaptcha extends Entity implements CaptchaInterface
/** @var string */
public string $siteKey;
/** @var string */
public string $apiUrl;
public string $siteSecret;
/** @var string */
public string $secretKey;
public string $challengeUrl;
/** @var string */
public string $puzzle;
public string $verifyUrl;
/**
* Constructor.
*/
public function __construct()
{
$this->service = 'AltchaCaptcha';
$this->siteKey = \App::$ALTCHA_CAPTCHA_SITE_KEY;
$this->apiUrl = \App::$ALTCHA_CAPTCHA_ENDPOINT;
$this->secretKey = \App::$ALTCHA_CAPTCHA_SECRET_KEY;
$this->puzzle = \App::$ALTCHA_CAPTCHA_ENDPOINT_PUZZLE;
$this->siteSecret = \App::$ALTCHA_CAPTCHA_SITE_SECRET;
$this->challengeUrl = \App::$ALTCHA_CAPTCHA_ENDPOINT_CHALLENGE;
$this->verifyUrl = \App::$ALTCHA_CAPTCHA_ENDPOINT_VERIFY;
$this->ensureValid();
}

Expand All @@ -50,34 +50,35 @@ public function getCaptchaDetails(): array
{
return [
'siteKey' => $this->siteKey,
'captchaEndpoint' => $this->apiUrl,
'puzzle' => $this->puzzle,
'captchaChallenge' => $this->challengeUrl,
'captchaVerify' => $this->verifyUrl,
'captchaEnabled' => \App::$CAPTCHA_ENABLED
];
}

/**
* Überprüft die Captcha-Lösung.
*
* @param string $solution
* @param string $payload
* @return bool
* @throws \Exception
*/
public function verifyCaptcha(string $solution): bool
public function verifyCaptcha(string $payload): bool
{
try {
$response = \App::$http->post($this->apiUrl, [
'form_params' => [
'secret' => $this->secretKey,
'solution' => $solution
$response = \App::$http->post($this->verifyUrl, [
'json' => [
'siteKey' => $this->siteKey,
'siteSecret' => $this->siteSecret,
'payload' => $payload
]
]);

$responseBody = json_decode((string)$response->getBody(), true);
if (json_last_error() !== JSON_ERROR_NONE || !isset($responseBody['valid'])) {
if (json_last_error() !== JSON_ERROR_NONE || !isset($responseBody['success'])) {
return false;
}

return $responseBody['valid'] === true;
return $responseBody['success'] === true;
} catch (RequestException $e) {
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace BO\Zmscitizenapi\Services\Security;

use BO\Zmscitizenapi\Models\Captcha\FriendlyCaptcha;
use BO\Zmscitizenapi\Models\Captcha\AltchaCaptcha;

class CaptchaService
{
Expand All @@ -13,8 +13,8 @@ public function getCaptcha(): array
return $this->getCaptchaDetails()->getCaptchaDetails();
}

private function getCaptchaDetails(): FriendlyCaptcha
private function getCaptchaDetails(): AltchaCaptcha
{
return new FriendlyCaptcha();
return new AltchaCaptcha();
}
}
18 changes: 9 additions & 9 deletions zmscitizenapi/tests/Zmscitizenapi/ApplicationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,10 @@ public function testInitializeCaptcha(): void
putenv('FRIENDLY_CAPTCHA_SITE_KEY=test_site');
putenv('FRIENDLY_CAPTCHA_ENDPOINT=https://test.example.com/verify');
putenv('FRIENDLY_CAPTCHA_ENDPOINT_PUZZLE=https://test.example.com/puzzle');
putenv('ALTCHA_CAPTCHA_SECRET_KEY=alt_secret');
putenv('ALTCHA_CAPTCHA_SITE_KEY=alt_site');
putenv('ALTCHA_CAPTCHA_ENDPOINT=https://test.altcha.org/form');
putenv('ALTCHA_CAPTCHA_ENDPOINT_PUZZLE=https://test.altcha.org');
putenv('ALTCHA_CAPTCHA_SITE_SECRET=alt_secret');
putenv('ALTCHA_CAPTCHA_ENDPOINT_CHALLENGE=https://captcha-k.muenchen.de/api/v1/captcha/challenge');
putenv('ALTCHA_CAPTCHA_ENDPOINT_VERIFY=https://captcha-k.muenchen.de/api/v1/captcha/verify');

Application::initialize();

Expand All @@ -110,10 +110,10 @@ public function testInitializeCaptcha(): void
$this->assertEquals('test_site', Application::$FRIENDLY_CAPTCHA_SITE_KEY);
$this->assertEquals('https://test.example.com/verify', Application::$FRIENDLY_CAPTCHA_ENDPOINT);
$this->assertEquals('https://test.example.com/puzzle', Application::$FRIENDLY_CAPTCHA_ENDPOINT_PUZZLE);
$this->assertEquals('alt_secret', Application::$ALTCHA_CAPTCHA_SECRET_KEY);
$this->assertEquals('alt_site', Application::$ALTCHA_CAPTCHA_SITE_KEY);
$this->assertEquals('https://test.altcha.org/form', Application::$ALTCHA_CAPTCHA_ENDPOINT);
$this->assertEquals('https://test.altcha.org', Application::$ALTCHA_CAPTCHA_ENDPOINT_PUZZLE);
$this->assertEquals('alt_secret', Application::$ALTCHA_CAPTCHA_SITE_SECRET);
$this->assertEquals('https://captcha-k.muenchen.de/api/v1/captcha/challenge', Application::$ALTCHA_CAPTCHA_ENDPOINT_CHALLENGE);
$this->assertEquals('https://captcha-k.muenchen.de/api/v1/captcha/verify', Application::$ALTCHA_CAPTCHA_ENDPOINT_VERIFY);
}

public function testInitializeCache(): void
Expand Down Expand Up @@ -228,10 +228,10 @@ protected function tearDown(): void
putenv('FRIENDLY_CAPTCHA_SITE_KEY');
putenv('FRIENDLY_CAPTCHA_ENDPOINT');
putenv('FRIENDLY_CAPTCHA_ENDPOINT_PUZZLE');
putenv('ALTCHA_CAPTCHA_SECRET_KEY');
putenv('ALTCHA_CAPTCHA_SITE_KEY');
putenv('ALTCHA_CAPTCHA_ENDPOINT');
putenv('ALTCHA_CAPTCHA_ENDPOINT_PUZZLE');
putenv('ALTCHA_CAPTCHA_SITE_SECRET');
putenv('ALTCHA_CAPTCHA_ENDPOINT_CHALLENGE');
putenv('ALTCHA_CAPTCHA_ENDPOINT_VERIFY');
putenv('CACHE_DIR');
putenv('SOURCE_CACHE_TTL');
putenv('RATE_LIMIT_MAX_REQUESTS');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

use BO\Zmscitizenapi\Tests\ControllerTestCase;

use BO\Zmscitizenapi\Models\Captcha\FriendlyCaptcha;
use BO\Zmscitizenapi\Models\Captcha\AltchaCaptcha;

class FriendlyCaptchaControllerTest extends ControllerTestCase
class AltchaCaptchaControllerTest extends ControllerTestCase
{
protected $classname = "\BO\Zmscitizenapi\Controllers\Security\CaptchaController";

Expand All @@ -20,19 +20,19 @@ public function setUp(): void
\App::$cache->clear();
}

putenv('FRIENDLY_CAPTCHA_SITE_KEY=FAKE_SITE_KEY');
putenv('FRIENDLY_CAPTCHA_ENDPOINT=https://api.friendlycaptcha.com/api/v1/siteverify');
putenv('FRIENDLY_CAPTCHA_ENDPOINT_PUZZLE=https://api.friendlycaptcha.com/api/v1/puzzle');
putenv('ALTCHA_CAPTCHA_SITE_KEY=FAKE_SITE_KEY');
putenv('ALTCHA_CAPTCHA_ENDPOINT_CHALLENGE=https://captcha-k.muenchen.de/api/v1/captcha/challenge');
putenv('ALTCHA_CAPTCHA_ENDPOINT_VERIFY=https://captcha-k.muenchen.de/api/v1/captcha/verify');
putenv('CAPTCHA_ENABLED=1');

\App::initialize();
}

public function tearDown(): void
{
putenv('FRIENDLY_CAPTCHA_SITEKEY=');
putenv('FRIENDLY_CAPTCHA_ENDPOINT=');
putenv('FRIENDLY_CAPTCHA_ENDPOINT_PUZZLE=');
putenv('ALTCHA_CAPTCHA_SITE_KEY=');
putenv('ALTCHA_CAPTCHA_ENDPOINT_VERIFY=');
putenv('ALTCHA_CAPTCHA_ENDPOINT_CHALLENGE=');
putenv('CAPTCHA_ENABLED=');

parent::tearDown();
Expand All @@ -47,8 +47,8 @@ public function testCaptchaDetails()

$expectedResponse = [
'siteKey' => 'FAKE_SITE_KEY',
'captchaEndpoint' => 'https://api.friendlycaptcha.com/api/v1/siteverify',
'puzzle' => 'https://api.friendlycaptcha.com/api/v1/puzzle',
'captchaChallenge' => 'https://captcha-k.muenchen.de/api/v1/captcha/challenge',
'captchaVerify' => 'https://captcha-k.muenchen.de/api/v1/captcha/verify',
'captchaEnabled' => true
];

Expand All @@ -62,8 +62,9 @@ public function testVerifyCaptchaSuccess()
$mockResponse = new \GuzzleHttp\Psr7\Response(200, [], json_encode(['success' => true]));
\App::$http = new \GuzzleHttp\Client(['handler' => \GuzzleHttp\HandlerStack::create(new \GuzzleHttp\Handler\MockHandler([$mockResponse]))]);

$captcha = new FriendlyCaptcha();
$captcha = new AltchaCaptcha();
$result = $captcha->verifyCaptcha('valid_solution');
var_dump($result);
$this->assertTrue($result);
}

Expand All @@ -73,7 +74,7 @@ public function testVerifyCaptchaFailure()
$mockResponse = new \GuzzleHttp\Psr7\Response(200, [], json_encode(['success' => false]));
\App::$http = new \GuzzleHttp\Client(['handler' => \GuzzleHttp\HandlerStack::create(new \GuzzleHttp\Handler\MockHandler([$mockResponse]))]);

$captcha = new FriendlyCaptcha();
$captcha = new AltchaCaptcha();
$result = $captcha->verifyCaptcha('invalid_solution');
$this->assertFalse($result);
}
Expand All @@ -86,7 +87,7 @@ public function testVerifyCaptchaException()
]);
\App::$http = new \GuzzleHttp\Client(['handler' => \GuzzleHttp\HandlerStack::create($mockHandler)]);

$captcha = new FriendlyCaptcha();
$captcha = new AltchaCaptcha();
$result = $captcha->verifyCaptcha('exception_solution');
$this->assertFalse($result);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,26 +28,26 @@ public function testGetCaptchaReturnsCaptchaDetails(): void
// Assert
$this->assertIsArray($result);
$this->assertArrayHasKey('siteKey', $result);
$this->assertArrayHasKey('captchaEndpoint', $result);
$this->assertArrayHasKey('puzzle', $result);
$this->assertArrayHasKey('captchaChallenge', $result);
$this->assertArrayHasKey('captchaVerify', $result);
$this->assertArrayHasKey('captchaEnabled', $result);
}

public function testGetCaptchaReturnsExpectedValues(): void
{
// Arrange
$expectedSiteKey = \App::$FRIENDLY_CAPTCHA_SITE_KEY;
$expectedEndpoint = \App::$FRIENDLY_CAPTCHA_ENDPOINT;
$expectedPuzzle = \App::$FRIENDLY_CAPTCHA_ENDPOINT_PUZZLE;
$expectedSiteKey = \App::$ALTCHA_CAPTCHA_SITE_KEY;
$expectedChallengeUrl = \App::$ALTCHA_CAPTCHA_ENDPOINT_CHALLENGE;
$expectedVerifyUrl = \App::$ALTCHA_CAPTCHA_ENDPOINT_VERIFY;
$expectedEnabled = \App::$CAPTCHA_ENABLED;

// Act
$result = $this->captchaService->getCaptcha();

// Assert
$this->assertEquals($expectedSiteKey, $result['siteKey']);
$this->assertEquals($expectedEndpoint, $result['captchaEndpoint']);
$this->assertEquals($expectedPuzzle, $result['puzzle']);
$this->assertEquals($expectedChallengeUrl, $result['captchaChallenge']);
$this->assertEquals($expectedVerifyUrl, $result['captchaVerify']);
$this->assertEquals($expectedEnabled, $result['captchaEnabled']);
}
}
30 changes: 30 additions & 0 deletions zmscitizenview/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions zmscitizenview/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
},
"dependencies": {
"@muenchen/muc-patternlab-vue": "4.0.0-beta.1",
"altcha": "^1.2.0",
"jsdom": "^26.0.0",
"vue": "^3.5.12",
"vue-i18n": "10.0.5",
Expand Down
12 changes: 12 additions & 0 deletions zmscitizenview/src/api/ZMSAppointmentAPI.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AppointmentDTO } from "@/api/models/AppointmentDTO";
import { AvailableDaysDTO } from "@/api/models/AvailableDaysDTO";
import { AvailableTimeSlotsDTO } from "@/api/models/AvailableTimeSlotsDTO";
import { CaptchaDetails } from "@/api/models/CaptchaDetails";
import { ErrorDTO } from "@/api/models/ErrorDTO";
import { OfficesAndServicesDTO } from "@/api/models/OfficesAndServicesDTO";
import { AppointmentHash } from "@/types/AppointmentHashTypes";
Expand All @@ -11,6 +12,7 @@ import {
VUE_APP_ZMS_API_AVAILABLE_TIME_SLOTS_ENDPOINT,
VUE_APP_ZMS_API_CALENDAR_ENDPOINT,
VUE_APP_ZMS_API_CANCEL_APPOINTMENT_ENDPOINT,
VUE_APP_ZMS_API_CAPTCHA_DETAILS_ENDPOINT,
VUE_APP_ZMS_API_CONFIRM_APPOINTMENT_ENDPOINT,
VUE_APP_ZMS_API_PRECONFIRM_APPOINTMENT_ENDPOINT,
VUE_APP_ZMS_API_PROVIDERS_AND_SERVICES_ENDPOINT,
Expand Down Expand Up @@ -236,3 +238,13 @@ export function cancelAppointment(
return response.json();
});
}

export function fetchCaptchaDetails(
baseUrl?: string
): Promise<CaptchaDetails | ErrorDTO> {
return fetch(
getAPIBaseURL(baseUrl) + VUE_APP_ZMS_API_CAPTCHA_DETAILS_ENDPOINT
).then((response) => {
return response.json();
});
}
Loading
Loading