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_SECRET=
|
||||||
MAILJET_API_BASE_URL=https://api.mailjet.com/v3/REST
|
MAILJET_API_BASE_URL=https://api.mailjet.com/v3/REST
|
||||||
MAILJET_DEFAULT_LIST_ID=
|
MAILJET_DEFAULT_LIST_ID=
|
||||||
# Development default for HTTP Basic Auth password "secret"; override in production secrets.
|
# Development default for HTTP Basic Auth password; override in production secrets.
|
||||||
MAILJET_WEBHOOK_BASIC_PASSWORD_HASH='$2y$10$uCeNsfVsTkGhylniiZTtGuzs3TDhFr7k/V36w6kD4LWHWbHuleDpa'
|
MAILJET_WEBHOOK_BASIC_PASSWORD=secret
|
||||||
APP_NEWSLETTER_CONFIRMATION_TTL_HOURS=1
|
APP_NEWSLETTER_CONFIRMATION_TTL_HOURS=1
|
||||||
|
|
||||||
APP_BPN_USER=
|
APP_BPN_USER=
|
||||||
|
|||||||
@@ -4,4 +4,4 @@ APP_SECRET='$ecretf0rt3st'
|
|||||||
SYMFONY_DEPRECATIONS_HELPER=999999
|
SYMFONY_DEPRECATIONS_HELPER=999999
|
||||||
PANTHER_APP_ENV=panther
|
PANTHER_APP_ENV=panther
|
||||||
PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots
|
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:
|
security:
|
||||||
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
|
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
|
||||||
password_hashers:
|
password_hashers:
|
||||||
|
Symfony\Component\Security\Core\User\InMemoryUser: 'plaintext'
|
||||||
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
|
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
|
||||||
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
|
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
|
||||||
providers:
|
providers:
|
||||||
@@ -12,7 +13,7 @@ security:
|
|||||||
memory:
|
memory:
|
||||||
users:
|
users:
|
||||||
mailjet:
|
mailjet:
|
||||||
password: '%env(MAILJET_WEBHOOK_BASIC_PASSWORD_HASH)%'
|
password: '%env(MAILJET_WEBHOOK_BASIC_PASSWORD)%'
|
||||||
roles: ['ROLE_MAILJET_WEBHOOK']
|
roles: ['ROLE_MAILJET_WEBHOOK']
|
||||||
firewalls:
|
firewalls:
|
||||||
dev:
|
dev:
|
||||||
@@ -58,6 +59,7 @@ when@test:
|
|||||||
# important to generate secure password hashes. In tests however, secure hashes
|
# important to generate secure password hashes. In tests however, secure hashes
|
||||||
# are not important, waste resources and increase test times. The following
|
# are not important, waste resources and increase test times. The following
|
||||||
# reduces the work factor to the lowest possible values.
|
# reduces the work factor to the lowest possible values.
|
||||||
|
Symfony\Component\Security\Core\User\InMemoryUser: 'plaintext'
|
||||||
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
|
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
|
||||||
algorithm: auto
|
algorithm: auto
|
||||||
cost: 4 # Lowest possible value for bcrypt
|
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`.
|
Creates or deletes the Mailjet `unsub` callback for `POST /webhooks/mailjet/newsletter`.
|
||||||
|
|
||||||
- Uses `APP_BASE_URL` by default and appends the webhook path.
|
- 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.
|
- Pass `--url` to target a different full webhook URL.
|
||||||
- `register` prints the API response payload so you can note the callback ID.
|
- `register` prints the API response payload so you can note the callback ID.
|
||||||
- `remove` and `deactivate` require the callback ID returned by Mailjet.
|
- `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_REMOVE = 'remove';
|
||||||
private const string ACTION_DEACTIVATE = 'deactivate';
|
private const string ACTION_DEACTIVATE = 'deactivate';
|
||||||
private const string EVENT_TYPE_UNSUBSCRIBE = 'unsub';
|
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 string WEBHOOK_PATH = '/webhooks/mailjet/newsletter';
|
||||||
private const int WEBHOOK_VERSION = 2;
|
private const int WEBHOOK_VERSION = 2;
|
||||||
|
|
||||||
@@ -33,6 +34,8 @@ final class MailjetNewsletterWebhookCommand extends Command
|
|||||||
private readonly MailjetApiClient $mailjetApiClient,
|
private readonly MailjetApiClient $mailjetApiClient,
|
||||||
#[Autowire('%env(APP_BASE_URL)%')]
|
#[Autowire('%env(APP_BASE_URL)%')]
|
||||||
private readonly string $defaultBaseUrl,
|
private readonly string $defaultBaseUrl,
|
||||||
|
#[Autowire('%env(MAILJET_WEBHOOK_BASIC_PASSWORD)%')]
|
||||||
|
private readonly string $basicAuthPassword,
|
||||||
private readonly LoggerInterface $logger,
|
private readonly LoggerInterface $logger,
|
||||||
) {
|
) {
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
@@ -43,7 +46,7 @@ final class MailjetNewsletterWebhookCommand extends Command
|
|||||||
$this
|
$this
|
||||||
->addArgument('action', InputArgument::REQUIRED, 'One of: register, remove, deactivate')
|
->addArgument('action', InputArgument::REQUIRED, 'One of: register, remove, deactivate')
|
||||||
->addArgument('callback_id', InputArgument::OPTIONAL, 'Callback ID required for 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
|
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
|
private function register(SymfonyStyle $io, mixed $overrideUrl): int
|
||||||
{
|
{
|
||||||
$webhookUrl = $this->resolveWebhookUrl($overrideUrl);
|
$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));
|
$io->writeln($this->formatPayload($payload));
|
||||||
|
|
||||||
return Command::SUCCESS;
|
return Command::SUCCESS;
|
||||||
@@ -115,6 +119,47 @@ final class MailjetNewsletterWebhookCommand extends Command
|
|||||||
return rtrim(trim($this->defaultBaseUrl), '/').self::WEBHOOK_PATH;
|
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
|
private function resolveCallbackId(mixed $callbackId): int
|
||||||
{
|
{
|
||||||
if (true === is_int($callbackId) && $callbackId > 0) {
|
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'])]
|
#[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);
|
$payload = json_decode($request->getContent(), true);
|
||||||
if (JSON_ERROR_NONE !== json_last_error() || false === is_array($payload)) {
|
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>>
|
* @return list<array<string, mixed>>
|
||||||
*/
|
*/
|
||||||
private function normalizeEvents(array $payload): array
|
private function normalizeEvents(array $payload): array
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class MailjetNewsletterWebhookCommandTest extends TestCase
|
|||||||
->method('createEventCallbackUrl')
|
->method('createEventCallbackUrl')
|
||||||
->with(
|
->with(
|
||||||
'unsub',
|
'unsub',
|
||||||
'https://my.ep-reisen.de/webhooks/mailjet/newsletter',
|
'https://mailjet:secret@my.ep-reisen.de/webhooks/mailjet/newsletter',
|
||||||
2,
|
2,
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
@@ -89,6 +89,7 @@ class MailjetNewsletterWebhookCommandTest extends TestCase
|
|||||||
return new MailjetNewsletterWebhookCommand(
|
return new MailjetNewsletterWebhookCommand(
|
||||||
$mailjetApiClient,
|
$mailjetApiClient,
|
||||||
'https://my.ep-reisen.de',
|
'https://my.ep-reisen.de',
|
||||||
|
'secret',
|
||||||
$this->createMock(LoggerInterface::class),
|
$this->createMock(LoggerInterface::class),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class MailjetNewsletterWebhookControllerTest extends TestCase
|
|||||||
->willReturnCallback(static fn (object $message): Envelope => new Envelope($message));
|
->willReturnCallback(static fn (object $message): Envelope => new Envelope($message));
|
||||||
|
|
||||||
$controller = new MailjetNewsletterWebhookController($messageBus, new NullLogger());
|
$controller = new MailjetNewsletterWebhookController($messageBus, new NullLogger());
|
||||||
$response = $controller(Request::create(
|
$response = $controller->index(Request::create(
|
||||||
'/webhooks/mailjet/newsletter',
|
'/webhooks/mailjet/newsletter',
|
||||||
'POST',
|
'POST',
|
||||||
content: json_encode([
|
content: json_encode([
|
||||||
@@ -61,7 +61,7 @@ class MailjetNewsletterWebhookControllerTest extends TestCase
|
|||||||
$messageBus->expects(self::never())->method('dispatch');
|
$messageBus->expects(self::never())->method('dispatch');
|
||||||
|
|
||||||
$controller = new MailjetNewsletterWebhookController($messageBus, new NullLogger());
|
$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());
|
self::assertSame(400, $response->getStatusCode());
|
||||||
}
|
}
|
||||||
@@ -72,7 +72,7 @@ class MailjetNewsletterWebhookControllerTest extends TestCase
|
|||||||
$messageBus->expects(self::never())->method('dispatch');
|
$messageBus->expects(self::never())->method('dispatch');
|
||||||
|
|
||||||
$controller = new MailjetNewsletterWebhookController($messageBus, new NullLogger());
|
$controller = new MailjetNewsletterWebhookController($messageBus, new NullLogger());
|
||||||
$response = $controller(Request::create(
|
$response = $controller->index(Request::create(
|
||||||
'/webhooks/mailjet/newsletter',
|
'/webhooks/mailjet/newsletter',
|
||||||
'POST',
|
'POST',
|
||||||
content: json_encode([
|
content: json_encode([
|
||||||
|
|||||||
Reference in New Issue
Block a user