fix: use plain password in configuration for MailJet webhook url
This commit is contained in:
@@ -44,8 +44,8 @@ MAILJET_API_KEY=
|
||||
MAILJET_API_SECRET=
|
||||
MAILJET_API_BASE_URL=https://api.mailjet.com/v3/REST
|
||||
MAILJET_DEFAULT_LIST_ID=
|
||||
# Development default for HTTP Basic Auth password "secret"; override in production secrets.
|
||||
MAILJET_WEBHOOK_BASIC_PASSWORD_HASH='$2y$10$uCeNsfVsTkGhylniiZTtGuzs3TDhFr7k/V36w6kD4LWHWbHuleDpa'
|
||||
# Development default for HTTP Basic Auth password; override in production secrets.
|
||||
MAILJET_WEBHOOK_BASIC_PASSWORD=secret
|
||||
APP_NEWSLETTER_CONFIRMATION_TTL_HOURS=1
|
||||
|
||||
APP_BPN_USER=
|
||||
|
||||
@@ -4,4 +4,4 @@ APP_SECRET='$ecretf0rt3st'
|
||||
SYMFONY_DEPRECATIONS_HELPER=999999
|
||||
PANTHER_APP_ENV=panther
|
||||
PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots
|
||||
MAILJET_WEBHOOK_BASIC_PASSWORD_HASH='$2y$10$uCeNsfVsTkGhylniiZTtGuzs3TDhFr7k/V36w6kD4LWHWbHuleDpa'
|
||||
MAILJET_WEBHOOK_BASIC_PASSWORD=secret
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
security:
|
||||
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
|
||||
password_hashers:
|
||||
Symfony\Component\Security\Core\User\InMemoryUser: 'plaintext'
|
||||
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
|
||||
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
|
||||
providers:
|
||||
@@ -12,7 +13,7 @@ security:
|
||||
memory:
|
||||
users:
|
||||
mailjet:
|
||||
password: '%env(MAILJET_WEBHOOK_BASIC_PASSWORD_HASH)%'
|
||||
password: '%env(MAILJET_WEBHOOK_BASIC_PASSWORD)%'
|
||||
roles: ['ROLE_MAILJET_WEBHOOK']
|
||||
firewalls:
|
||||
dev:
|
||||
@@ -58,6 +59,7 @@ when@test:
|
||||
# important to generate secure password hashes. In tests however, secure hashes
|
||||
# are not important, waste resources and increase test times. The following
|
||||
# reduces the work factor to the lowest possible values.
|
||||
Symfony\Component\Security\Core\User\InMemoryUser: 'plaintext'
|
||||
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
|
||||
algorithm: auto
|
||||
cost: 4 # Lowest possible value for bcrypt
|
||||
|
||||
@@ -1169,6 +1169,8 @@ php bin/console app:mailjet:newsletter-webhook deactivate
|
||||
Creates or deletes the Mailjet `unsub` callback for `POST /webhooks/mailjet/newsletter`.
|
||||
|
||||
- Uses `APP_BASE_URL` by default and appends the webhook path.
|
||||
- The command prepends `mailjet:<password>@` to the URL before registering it with Mailjet.
|
||||
- Configure the plain password in `MAILJET_WEBHOOK_BASIC_PASSWORD`; Symfony uses it for the webhook firewall.
|
||||
- Pass `--url` to target a different full webhook URL.
|
||||
- `register` prints the API response payload so you can note the callback ID.
|
||||
- `remove` and `deactivate` require the callback ID returned by Mailjet.
|
||||
|
||||
@@ -26,6 +26,7 @@ final class MailjetNewsletterWebhookCommand extends Command
|
||||
private const string ACTION_REMOVE = 'remove';
|
||||
private const string ACTION_DEACTIVATE = 'deactivate';
|
||||
private const string EVENT_TYPE_UNSUBSCRIBE = 'unsub';
|
||||
private const string WEBHOOK_BASIC_AUTH_USER = 'mailjet';
|
||||
private const string WEBHOOK_PATH = '/webhooks/mailjet/newsletter';
|
||||
private const int WEBHOOK_VERSION = 2;
|
||||
|
||||
@@ -33,6 +34,8 @@ final class MailjetNewsletterWebhookCommand extends Command
|
||||
private readonly MailjetApiClient $mailjetApiClient,
|
||||
#[Autowire('%env(APP_BASE_URL)%')]
|
||||
private readonly string $defaultBaseUrl,
|
||||
#[Autowire('%env(MAILJET_WEBHOOK_BASIC_PASSWORD)%')]
|
||||
private readonly string $basicAuthPassword,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
parent::__construct();
|
||||
@@ -43,7 +46,7 @@ final class MailjetNewsletterWebhookCommand extends Command
|
||||
$this
|
||||
->addArgument('action', InputArgument::REQUIRED, 'One of: register, remove, deactivate')
|
||||
->addArgument('callback_id', InputArgument::OPTIONAL, 'Callback ID required for remove/deactivate')
|
||||
->addOption('url', null, InputOption::VALUE_REQUIRED, 'Override the webhook URL to register');
|
||||
->addOption('url', null, InputOption::VALUE_REQUIRED, 'Override the webhook URL to register before Basic Auth credentials are added');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
@@ -72,9 +75,10 @@ final class MailjetNewsletterWebhookCommand extends Command
|
||||
private function register(SymfonyStyle $io, mixed $overrideUrl): int
|
||||
{
|
||||
$webhookUrl = $this->resolveWebhookUrl($overrideUrl);
|
||||
$payload = $this->mailjetApiClient->createEventCallbackUrl(self::EVENT_TYPE_UNSUBSCRIBE, $webhookUrl, self::WEBHOOK_VERSION);
|
||||
$authenticatedWebhookUrl = $this->prependBasicAuth($webhookUrl);
|
||||
$payload = $this->mailjetApiClient->createEventCallbackUrl(self::EVENT_TYPE_UNSUBSCRIBE, $authenticatedWebhookUrl, self::WEBHOOK_VERSION);
|
||||
|
||||
$io->success(sprintf('Created Mailjet newsletter webhook at %s.', $webhookUrl));
|
||||
$io->success(sprintf('Created Mailjet newsletter webhook at %s.', $authenticatedWebhookUrl));
|
||||
$io->writeln($this->formatPayload($payload));
|
||||
|
||||
return Command::SUCCESS;
|
||||
@@ -115,6 +119,47 @@ final class MailjetNewsletterWebhookCommand extends Command
|
||||
return rtrim(trim($this->defaultBaseUrl), '/').self::WEBHOOK_PATH;
|
||||
}
|
||||
|
||||
private function prependBasicAuth(string $url): string
|
||||
{
|
||||
if ('' === trim($this->basicAuthPassword)) {
|
||||
throw new \InvalidArgumentException('MAILJET_WEBHOOK_BASIC_PASSWORD is not configured.');
|
||||
}
|
||||
|
||||
$parts = parse_url($url);
|
||||
if (false === is_array($parts) || false === isset($parts['scheme'], $parts['host'])) {
|
||||
throw new \InvalidArgumentException('The webhook URL is not valid.');
|
||||
}
|
||||
|
||||
$host = $parts['host'];
|
||||
if (false === str_starts_with($host, '[') && true === str_contains($host, ':')) {
|
||||
$host = sprintf('[%s]', $host);
|
||||
}
|
||||
|
||||
$authenticatedUrl = sprintf(
|
||||
'%s://%s:%s@%s',
|
||||
$parts['scheme'],
|
||||
rawurlencode(self::WEBHOOK_BASIC_AUTH_USER),
|
||||
rawurlencode($this->basicAuthPassword),
|
||||
$host,
|
||||
);
|
||||
|
||||
if (isset($parts['port'])) {
|
||||
$authenticatedUrl .= sprintf(':%d', $parts['port']);
|
||||
}
|
||||
|
||||
$authenticatedUrl .= $parts['path'] ?? '';
|
||||
|
||||
if (isset($parts['query'])) {
|
||||
$authenticatedUrl .= '?'.$parts['query'];
|
||||
}
|
||||
|
||||
if (isset($parts['fragment'])) {
|
||||
$authenticatedUrl .= '#'.$parts['fragment'];
|
||||
}
|
||||
|
||||
return $authenticatedUrl;
|
||||
}
|
||||
|
||||
private function resolveCallbackId(mixed $callbackId): int
|
||||
{
|
||||
if (true === is_int($callbackId) && $callbackId > 0) {
|
||||
|
||||
@@ -22,7 +22,7 @@ final class MailjetNewsletterWebhookController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route('/webhooks/mailjet/newsletter', name: 'app_webhook_mailjet_newsletter', methods: ['POST'])]
|
||||
public function __invoke(Request $request): JsonResponse
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$payload = json_decode($request->getContent(), true);
|
||||
if (JSON_ERROR_NONE !== json_last_error() || false === is_array($payload)) {
|
||||
@@ -51,8 +51,7 @@ final class MailjetNewsletterWebhookController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<mixed> $payload
|
||||
*
|
||||
* @param array<string, mixed> $payload
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
private function normalizeEvents(array $payload): array
|
||||
|
||||
@@ -20,7 +20,7 @@ class MailjetNewsletterWebhookCommandTest extends TestCase
|
||||
->method('createEventCallbackUrl')
|
||||
->with(
|
||||
'unsub',
|
||||
'https://my.ep-reisen.de/webhooks/mailjet/newsletter',
|
||||
'https://mailjet:secret@my.ep-reisen.de/webhooks/mailjet/newsletter',
|
||||
2,
|
||||
false,
|
||||
)
|
||||
@@ -89,6 +89,7 @@ class MailjetNewsletterWebhookCommandTest extends TestCase
|
||||
return new MailjetNewsletterWebhookCommand(
|
||||
$mailjetApiClient,
|
||||
'https://my.ep-reisen.de',
|
||||
'secret',
|
||||
$this->createMock(LoggerInterface::class),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ class MailjetNewsletterWebhookControllerTest extends TestCase
|
||||
->willReturnCallback(static fn (object $message): Envelope => new Envelope($message));
|
||||
|
||||
$controller = new MailjetNewsletterWebhookController($messageBus, new NullLogger());
|
||||
$response = $controller(Request::create(
|
||||
$response = $controller->index(Request::create(
|
||||
'/webhooks/mailjet/newsletter',
|
||||
'POST',
|
||||
content: json_encode([
|
||||
@@ -61,7 +61,7 @@ class MailjetNewsletterWebhookControllerTest extends TestCase
|
||||
$messageBus->expects(self::never())->method('dispatch');
|
||||
|
||||
$controller = new MailjetNewsletterWebhookController($messageBus, new NullLogger());
|
||||
$response = $controller(Request::create('/webhooks/mailjet/newsletter', 'POST', content: '{'));
|
||||
$response = $controller->index(Request::create('/webhooks/mailjet/newsletter', 'POST', content: '{'));
|
||||
|
||||
self::assertSame(400, $response->getStatusCode());
|
||||
}
|
||||
@@ -72,7 +72,7 @@ class MailjetNewsletterWebhookControllerTest extends TestCase
|
||||
$messageBus->expects(self::never())->method('dispatch');
|
||||
|
||||
$controller = new MailjetNewsletterWebhookController($messageBus, new NullLogger());
|
||||
$response = $controller(Request::create(
|
||||
$response = $controller->index(Request::create(
|
||||
'/webhooks/mailjet/newsletter',
|
||||
'POST',
|
||||
content: json_encode([
|
||||
|
||||
Reference in New Issue
Block a user