feat: accept form submissions from the websites over an API endpoint

This commit is contained in:
fromme committed 2026-09-24 16:35:06 +02:00
1 parent f48e53fdd0
commit 30ad5e8488
17 files changed
+1350 -4

No files matched your search

@@ -0,0 +1,24 @@
<?php
namespace App\Security\AccessToken;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Http\AccessToken\AccessTokenExtractorInterface;
/**
* Reads the API key of the stateless "api" firewall from its own header. The built-in header
* extractor expects "Authorization: Bearer", which would claim a shared secret is an OAuth
* access token - this application is the client of a real OAuth provider (MYEP_OAUTH2_*), and
* the two must not look alike in a request.
*/
class ApiKeyExtractor implements AccessTokenExtractorInterface
{
public const HEADER = 'X-Api-Key';
public function extractAccessToken(Request $request): ?string
{
$key = trim((string) $request->headers->get(self::HEADER, ''));
return '' === $key ? null : $key;
}
}
@@ -0,0 +1,45 @@
<?php
namespace App\Security\AccessToken;
use Psr\Log\LoggerInterface;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
/**
* Validates the shared key a sibling website posts with. There is no person to log in and no
* account to look up: the key is the identity, and the user it resolves to is the in-memory
* "api" user of the api_user_provider - nothing about it may reach the user table, where a row
* would be a login to this application's UI.
*/
class ApiKeyTokenHandler implements AccessTokenHandlerInterface
{
public const USER_IDENTIFIER = 'api';
public function __construct(
private readonly string $apiKey,
private readonly LoggerInterface $logger,
) {
}
public function getUserBadgeFrom(string $accessToken): UserBadge
{
// An unconfigured key locks the endpoint instead of opening it: hash_equals('', '')
// is true, so without this check a deployment that forgot the secret would accept
// an empty key from anyone.
if ('' === $this->apiKey) {
$this->logger->error('API key authentication attempted while no key is configured');
throw new BadCredentialsException('No API key configured.');
}
if (false === hash_equals($this->apiKey, $accessToken)) {
$this->logger->warning('API key authentication failed');
throw new BadCredentialsException('Invalid API key.');
}
return new UserBadge(self::USER_IDENTIFIER);
}
}