diff --git a/src/Auth/ApiMandatoryHeadersMiddleware.php b/src/Auth/ApiMandatoryHeadersMiddleware.php new file mode 100644 index 0000000..16bdecb --- /dev/null +++ b/src/Auth/ApiMandatoryHeadersMiddleware.php @@ -0,0 +1,177 @@ +): PromiseInterface $nextHandler Next handler to invoke. + */ + public function __construct( + private $nextHandler, + private ClientIdentifier $apiKey, + private ClientSecret $withSecret + ) { + } + + /** @param array $options */ + public function __invoke(RequestInterface $request, array $options): PromiseInterface + { + $fn = $this->nextHandler; + + $signedRequest = $this->sign($request); + + return $fn($signedRequest, $options); + } + + private function sign(RequestInterface $request): RequestInterface + { + /** @var array $modifyMap */ + $modifyMap = []; + + $this->addClientIdHeader($request, $modifyMap); + $this->addTimestampHeader($request, $modifyMap); + $this->addNonceHeader($request, $modifyMap); + $this->addBodyHashHeader($request, $modifyMap); + $this->addSignatureHeader($request, $modifyMap, $this->withSecret); + + return Utils::modifyRequest($request, [ + 'set_headers' => $modifyMap, + ]); + } + + /** @param array $modifyMap */ + private function addClientIdHeader(RequestInterface $request, array &$modifyMap): void + { + if (! $request->hasHeader(self::HEADER_CLIENT_IDENTIFIER)) { + $modifyMap[self::HEADER_CLIENT_IDENTIFIER] = $this->apiKey->value; + } + } + + /** @param array $modifyMap */ + private function addTimestampHeader(RequestInterface $request, array &$modifyMap): void + { + if (! $request->hasHeader(self::HEADER_TIMESTAMP)) { + $modifyMap[self::HEADER_TIMESTAMP] = (new \DateTimeImmutable('now', new \DateTimeZone('UTC')))->format(\DateTimeInterface::ATOM); + } + } + + /** @param array $modifyMap */ + private function addNonceHeader(RequestInterface $request, array &$modifyMap): void + { + if (! $request->hasHeader(self::HEADER_NONCE)) { + $modifyMap[self::HEADER_NONCE] = $this->generateNonce()->value->toString(); + } + } + + /** @param array $modifyMap */ + private function addBodyHashHeader(RequestInterface $request, array &$modifyMap): void + { + if (! $request->hasHeader(self::HEADER_BODY_HASH)) { + $body = $request->getBody(); + $body->rewind(); + $hash = hash('sha256', $body->getContents()); + $modifyMap[self::HEADER_BODY_HASH] = $hash; + } + } + + /** @param array $modifyMap */ + private function addSignatureHeader( + RequestInterface $request, + array &$modifyMap, + ClientSecret $withSecret + ): void { + $signaturePayload = $this->compileDataToSign($request, $modifyMap); + $signature = $this->calculateSignature($signaturePayload, $withSecret); + $modifyMap[self::HEADER_SIGNATURE] = $signature; + } + + /** @param list $signaturePayload */ + private function calculateSignature(array $signaturePayload, ClientSecret $withSecret): string + { + $payloadString = implode('|', $signaturePayload); + + return hash_hmac('sha256', $payloadString, $withSecret->value); + } + + /** + * @param array $modifyMap + * @return list + */ + private function compileDataToSign(RequestInterface $request, array $modifyMap): array + { + $timestamp = $modifyMap[self::HEADER_TIMESTAMP] ?? $request->getHeaderLine(self::HEADER_TIMESTAMP); + $nonce = $modifyMap[self::HEADER_NONCE] ?? $request->getHeaderLine(self::HEADER_NONCE); + $bodyHash = $modifyMap[self::HEADER_BODY_HASH] ?? ($request->getHeaderLine(self::HEADER_BODY_HASH) ?: self::SHA256_EMPTY_STRING_HASH); + + return [ + strtoupper($request->getMethod()), + $this->getOrdonatedPathAndQuery($request->getUri()), + $timestamp, + $nonce, + $bodyHash, + ]; + } + + private function getOrdonatedPathAndQuery(Uri $fromUri): string + { + $path = $fromUri->getPath(); + $query = $fromUri->getQuery(); + + if ($query === '') { + return $path; + } + + // NOTE: Sort query parameters to ensure consistent signature generation + parse_str($query, $queryParams); + ksort($queryParams); + $sortedQuery = http_build_query($queryParams); + + return "{$path}?{$sortedQuery}"; + } + + private function generateNonce(): Nonce + { + return new Nonce( + value: Uuid::uuid7(), + ); + } +} diff --git a/src/Auth/ClientIdentifier.php b/src/Auth/ClientIdentifier.php new file mode 100644 index 0000000..4de473a --- /dev/null +++ b/src/Auth/ClientIdentifier.php @@ -0,0 +1,17 @@ +toString())) { + throw new \InvalidArgumentException('Nonce value is not a valid UUID'); + } + } +}