2.1.0 - timing-safe payment-window verification, strict_types
Security and correctness release on the payment-acceptance path, plus strict_types across the tree.
composer require tomsommer/onpay-php-sdk:^2.1Payment window verification
PaymentWindow::validatePayment() had three problems, all inherited from upstream:
- The HMAC was compared with
===, which is not timing-safe. It now useshash_equals(). - Signed fields were selected by substring, not prefix. The window writes its fields with an
onpay_prefix, but verification collected anything matchingstrpos($key, 'onpay_') !== false. An unrelated query parameter that merely contained the substring —tracking_onpay_campaign, for instance — was pulled into the comparison set and broke verification of a perfectly good payment. Selection is now by prefix. There is a regression test that fails against the old code. - An array-valued
onpay_hmac_sha1reached the comparison.?onpay_hmac_sha1[]=xis trivially injectable andhash_equals()rejects a non-string with aTypeError, so non-strings are now turned away before the compare.
The window secret is also typed and required where it is used. Hashing with an empty key produced a signature that could never match, so getFormFields(), generateSecret() and validatePayment() now raise MissingDataException rather than failing quietly.
Refresh tokens survive a refresh
RFC 6749 §6 permits a server to omit refresh_token from a refresh response, in which case the previous one stays valid. The SDK saved the response verbatim, dropping the refresh token and making the next expiry unrecoverable. The old token is now carried over.
strict_types
declare(strict_types=1) in all 61 files. Amounts travel through this SDK as numeric strings and get compared against ints, which is precisely where silent coercion does damage.
Breaking changes
PaymentWindow::setSecret()is typed?string;getSecret()returns?string.getFormFields(),generateSecret()andvalidatePayment()throwOnPay\API\Exception\MissingDataExceptionwhen no window secret has been set.- A query parameter containing
onpay_other than as a prefix is no longer part of the signed set. If you were relying on the old substring behaviour, you were relying on a bug.
Everything from 2.0.1 applies unchanged: league/oauth2-client, PSR-18 transport, PSR-3 logging, PSR-7 last-exchange accessors, PHP 8.2+.