├── .gitignore ├── resources ├── test_key.p12 └── test_key_container.p12 ├── src └── Developer │ ├── Signers │ ├── BaseSigner.php │ ├── CurlRequestSigner.php │ └── PsrHttpMessageSigner.php │ └── OAuth │ ├── Utils │ ├── SecurityUtils.php │ └── AuthenticationUtils.php │ └── OAuth.php ├── .github ├── pull_request_template.md └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── phpunit.xml ├── tests └── Developer │ ├── OAuth │ ├── Test │ │ └── TestUtils.php │ ├── Utils │ │ ├── SecurityUtilsTest.php │ │ └── AuthenticationUtilsTest.php │ └── OAuthTest.php │ └── Signers │ ├── PsrHttpMessageSignerTest.php │ └── CurlRequestSignerTest.php ├── composer.json ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | vendor 3 | tests.xml 4 | coverage.xml -------------------------------------------------------------------------------- /resources/test_key.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mastercard/oauth1-signer-php/HEAD/resources/test_key.p12 -------------------------------------------------------------------------------- /resources/test_key_container.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mastercard/oauth1-signer-php/HEAD/resources/test_key_container.p12 -------------------------------------------------------------------------------- /src/Developer/Signers/BaseSigner.php: -------------------------------------------------------------------------------- 1 | consumerKey = $consumerKey; 12 | $this->signingKey = $signingKey; 13 | } 14 | } -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | ### PR checklist 3 | 4 | - [ ] An issue/feature request has been created for this PR 5 | - [ ] Pull Request title clearly describes the work in the pull request and the Pull Request description provides details about how to validate the work. Missing information here may result in a delayed response. 6 | - [ ] File the PR against the `master` branch 7 | - [ ] The code in this PR is covered by unit tests 8 | 9 | #### Link to issue/feature request: *add the link here* 10 | 11 | #### Description 12 | A clear and concise description of what is this PR for and any additional info might be useful for reviewing it. 13 | -------------------------------------------------------------------------------- /src/Developer/OAuth/Utils/SecurityUtils.php: -------------------------------------------------------------------------------- 1 | consumerKey, $this->signingKey); 15 | $headers[] = OAuth::AUTHORIZATION_HEADER_NAME . ': ' . $authHeader; 16 | curl_setopt($handle, CURLOPT_HTTPHEADER, $headers); 17 | } 18 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[REQ] Feature Request Description" 5 | labels: 'Enhancement: Feature' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Is your feature request related to a problem? Please describe. 11 | 12 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 13 | 14 | ### Describe the solution you'd like 15 | 16 | A clear and concise description of what you want to happen. 17 | 18 | ### Describe alternatives you've considered 19 | 20 | A clear and concise description of any alternative solutions or features you've considered. 21 | 22 | ### Additional context 23 | 24 | Add any other context or screenshots about the feature request here. 25 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./tests 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | . 15 | 16 | vendor 17 | tests 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /tests/Developer/OAuth/Test/TestUtils.php: -------------------------------------------------------------------------------- 1 | getMethod($functionName); 15 | $method->setAccessible(true); 16 | return $method->invokeArgs(null, $args); 17 | } 18 | 19 | public static function getTestSigningKey() { 20 | return AuthenticationUtils::loadSigningKey('./resources/test_key_container.p12','mykeyalias', 'Password1'); 21 | } 22 | } -------------------------------------------------------------------------------- /src/Developer/Signers/PsrHttpMessageSigner.php: -------------------------------------------------------------------------------- 1 | getUri()->__toString(); 15 | $method = $request->getMethod(); 16 | $body = $request->getBody()->__toString(); 17 | $authHeader = OAuth::getAuthorizationHeader($uri, $method, $body , $this->consumerKey, $this->signingKey); 18 | $request = $request->withHeader(OAuth::AUTHORIZATION_HEADER_NAME, $authHeader); //NOSONAR 19 | return $request; 20 | } 21 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mastercard/oauth1-signer", 3 | "description": "Zero dependency library for generating a Mastercard API compliant OAuth signature.", 4 | "type": "library", 5 | "license": "MIT", 6 | "keywords": [ 7 | "openapi", 8 | "mastercard", 9 | "oauth1", 10 | "oauth1a", 11 | "php" 12 | ], 13 | "authors": [ 14 | { 15 | "name": "Mastercard" 16 | } 17 | ], 18 | "require": { 19 | }, 20 | "require-dev": { 21 | "yoast/phpunit-polyfills": "^1.0", 22 | "guzzlehttp/guzzle": "^6.2" 23 | }, 24 | "suggest": { 25 | "psr/http-message": "Allow usage of the PsrHttpMessageSigner class" 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "Mastercard\\": "src/" 30 | } 31 | }, 32 | "autoload-dev": { 33 | "psr-4": { 34 | "Mastercard\\": ["src/", "tests/"] 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Developer/OAuth/Utils/AuthenticationUtils.php: -------------------------------------------------------------------------------- 1 | getConstructor(); 16 | 17 | // WHEN 18 | $isPrivate = $constructor->isPrivate(); 19 | 20 | // THEN 21 | $this->assertTrue($isPrivate); 22 | 23 | // COVERAGE 24 | $constructor->setAccessible(true); 25 | $constructor->invoke($class->newInstanceWithoutConstructor()); 26 | } 27 | 28 | public function testLoadPrivateKey_ShouldReturnKey() { 29 | 30 | // GIVEN 31 | $keyContainerPath = './resources/test_key_container.p12'; 32 | $keyAlias = 'mykeyalias'; 33 | $keyPassword = 'Password1'; 34 | 35 | // WHEN 36 | $privateKey = SecurityUtils::loadPrivateKey($keyContainerPath, $keyAlias, $keyPassword); 37 | 38 | // THEN 39 | $this->assertNotEmpty($privateKey); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a bug report to help us improve 4 | title: "[BUG] Description" 5 | labels: 'Issue: Bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | #### Bug Report Checklist 11 | 12 | - [ ] Have you provided a code sample to reproduce the issue? 13 | - [ ] Have you tested with the latest release to confirm the issue still exists? 14 | - [ ] Have you searched for related issues/PRs? 15 | - [ ] What's the actual output vs expected output? 16 | 17 | 20 | 21 | **Description** 22 | A clear and concise description of what is the question, suggestion, or issue and why this is a problem for you. 23 | 24 | **To Reproduce** 25 | Steps to reproduce the behavior. 26 | 27 | **Expected behavior** 28 | A clear and concise description of what you expected to happen. 29 | 30 | **Screenshots** 31 | If applicable, add screenshots to help explain your problem. 32 | 33 | **Additional context** 34 | Add any other context about the problem here (OS, language version, etc..). 35 | 36 | 37 | **Related issues/PRs** 38 | Has a similar issue/PR been reported/opened before? 39 | 40 | **Suggest a fix/enhancement** 41 | If you can't fix the bug yourself, perhaps you can point to what might be causing the problem (line of code or commit), or simply make a suggestion. -------------------------------------------------------------------------------- /tests/Developer/Signers/PsrHttpMessageSignerTest.php: -------------------------------------------------------------------------------- 1 | 'application/json']; 18 | $request = new Request('POST', 'https://api.mastercard.com/service', $headers, $body); 19 | 20 | // WHEN 21 | $instanceUnderTest = new PsrHttpMessageSigner($consumerKey, $signingKey); 22 | $outRequest = $instanceUnderTest->sign($request); 23 | 24 | // THEN 25 | $this->assertNotEmpty($outRequest); 26 | $this->assertSame($outRequest, $request); 27 | $authorizationHeaderValue = $request->getHeader('Authorization')[0]; 28 | $this->assertNotEmpty($authorizationHeaderValue); 29 | $this->assertEquals(strlen('OAuth '), strpos($authorizationHeaderValue, 'oauth_consumer_key')); 30 | } 31 | 32 | public function testSign_ShouldAddOAuth1HeaderToGetRequest() { 33 | 34 | // GIVEN 35 | $signingKey = TestUtils::getTestSigningKey(); 36 | $consumerKey = 'Some key'; 37 | $request = new Request('GET', 'https://api.mastercard.com/service'); 38 | 39 | // WHEN 40 | $instanceUnderTest = new PsrHttpMessageSigner($consumerKey, $signingKey); 41 | $outRequest = $instanceUnderTest->sign($request); 42 | 43 | // THEN 44 | $this->assertNotEmpty($outRequest); 45 | $this->assertSame($outRequest, $request); 46 | $authorizationHeaderValue = $request->getHeader('Authorization')[0]; 47 | $this->assertNotEmpty($authorizationHeaderValue); 48 | $this->assertEquals(0, strpos($authorizationHeaderValue, 'OAuth')); 49 | } 50 | } -------------------------------------------------------------------------------- /tests/Developer/Signers/CurlRequestSignerTest.php: -------------------------------------------------------------------------------- 1 | 'bår']); 18 | $headers = array( 19 | 'Content-Type: application/json', 20 | 'Content-Length: ' . strlen($payload) 21 | ); 22 | $handle = curl_init($uri); 23 | curl_setopt_array($handle, array( 24 | CURLINFO_HEADER_OUT => 1, 25 | CURLOPT_RETURNTRANSFER => 1, 26 | CURLOPT_CUSTOMREQUEST => $method, 27 | CURLOPT_POSTFIELDS => $payload) 28 | ); 29 | 30 | // WHEN 31 | $instanceUnderTest = new CurlRequestSigner($consumerKey, $signingKey); 32 | $instanceUnderTest->sign($handle, $method, $headers, $payload); 33 | curl_exec($handle); // There is no way of reading HTTP headers without having the request successfully sent 34 | 35 | // THEN 36 | $headerInfo = curl_getinfo($handle, CURLINFO_HEADER_OUT); 37 | $this->assertTrue(strpos($headerInfo, 'Authorization: OAuth') > 0); 38 | } 39 | 40 | public function testSign_ShouldAddOAuth1HeaderToGetRequest() { 41 | 42 | // GIVEN 43 | $signingKey = TestUtils::getTestSigningKey(); 44 | $consumerKey = 'Some key'; 45 | $method = 'GET'; 46 | $uri = 'http://httpbin.org/'; 47 | $handle = curl_init($uri); 48 | curl_setopt_array($handle, array( 49 | CURLINFO_HEADER_OUT => 1, 50 | CURLOPT_RETURNTRANSFER => 1) 51 | ); 52 | // WHEN 53 | $instanceUnderTest = new CurlRequestSigner($consumerKey, $signingKey); 54 | $instanceUnderTest->sign($handle, $method); 55 | curl_exec($handle); // There is no way of reading HTTP headers without having the request successfully sent 56 | 57 | // THEN 58 | $headerInfo = curl_getinfo($handle, CURLINFO_HEADER_OUT); 59 | $this->assertTrue(strpos($headerInfo, 'Authorization: OAuth') > 0); 60 | } 61 | } -------------------------------------------------------------------------------- /tests/Developer/OAuth/Utils/AuthenticationUtilsTest.php: -------------------------------------------------------------------------------- 1 | getConstructor(); 13 | 14 | // WHEN 15 | $isPrivate = $constructor->isPrivate(); 16 | 17 | // THEN 18 | $this->assertTrue($isPrivate); 19 | 20 | // COVERAGE 21 | $constructor->setAccessible(true); 22 | $constructor->invoke($class->newInstanceWithoutConstructor()); 23 | } 24 | 25 | public function testLoadSigningKey_ShouldReturnKey() { 26 | 27 | // GIVEN 28 | $keyContainerPath = './resources/test_key_container.p12'; 29 | $keyAlias = 'mykeyalias'; 30 | $keyPassword = 'Password1'; 31 | 32 | // WHEN 33 | $privateKey = AuthenticationUtils::loadSigningKey($keyContainerPath, $keyAlias, $keyPassword); 34 | 35 | // THEN 36 | $this->assertNotEmpty($privateKey); 37 | } 38 | 39 | public function testLoadSigningKey_ShouldThrowInvalidArgumentException_WhenWrongPassword() { 40 | 41 | // THEN 42 | $this->expectException(\InvalidArgumentException::class); 43 | $this->expectExceptionMessage('Failed to open keystore with the provided password!'); 44 | 45 | // GIVEN 46 | $keyContainerPath = './resources/test_key_container.p12'; 47 | $keyAlias = 'mykeyalias'; 48 | $keyPassword = 'Wrong password'; 49 | 50 | // WHEN 51 | AuthenticationUtils::loadSigningKey($keyContainerPath, $keyAlias, $keyPassword); 52 | } 53 | 54 | public function testLoadSigningKey_ShouldThrowInvalidArgumentException_WhenFileDoesNotExists() { 55 | 56 | // THEN 57 | $this->expectException(\InvalidArgumentException::class); 58 | $this->expectExceptionMessage('Failed to read the given file: ./resources/some file'); 59 | 60 | // GIVEN 61 | $keyContainerPath = './resources/some file'; 62 | $keyAlias = 'mykeyalias'; 63 | $keyPassword = 'Password1'; 64 | 65 | // WHEN 66 | AuthenticationUtils::loadSigningKey($keyContainerPath, $keyAlias, $keyPassword); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # oauth1-signer-php 2 | [![](https://developer.mastercard.com/_/_/src/global/assets/svg/mcdev-logo-dark.svg)](https://developer.mastercard.com/) 3 | 4 | [![maintenance-status](https://img.shields.io/badge/maintenance-deprecated-red.svg)](https://github.com/Mastercard/.github/blob/main/CLIENT_LIBRARY_DEPRECATION_POLICY.md) 5 | [![](https://sonarcloud.io/api/project_badges/measure?project=Mastercard_oauth1-signer-php&metric=alert_status)](https://sonarcloud.io/dashboard?id=Mastercard_oauth1-signer-php) 6 | [![](https://img.shields.io/packagist/v/mastercard/oauth1-signer.svg)](https://packagist.org/packages/mastercard/oauth1-signer) 7 | [![](https://img.shields.io/badge/license-MIT-yellow.svg)](https://github.com/Mastercard/oauth1-signer-php/blob/master/LICENSE) 8 | 9 | ## Table of Contents 10 | - [Overview](#overview) 11 | * [Compatibility](#compatibility) 12 | * [References](#references) 13 | * [Versioning and Deprecation Policy](#versioning) 14 | - [Usage](#usage) 15 | * [Prerequisites](#prerequisites) 16 | * [Adding the Library to Your Project](#adding-the-library-to-your-project) 17 | * [Loading the Signing Key](#loading-the-signing-key) 18 | * [Creating the OAuth Authorization Header](#creating-the-oauth-authorization-header) 19 | * [Signing HTTP Client Request Objects](#signing-http-client-request-objects) 20 | * [Integrating with OpenAPI Generator API Client Libraries](#integrating-with-openapi-generator-api-client-libraries) 21 | 22 | ## Overview 23 | Zero dependency library for generating a Mastercard API compliant OAuth signature. 24 | 25 | ### Compatibility 26 | PHP 5.6+ 27 | 28 | ### References 29 | * [OAuth 1.0a specification](https://tools.ietf.org/html/rfc5849) 30 | * [Body hash extension for non application/x-www-form-urlencoded payloads](https://tools.ietf.org/id/draft-eaton-oauth-bodyhash-00.html) 31 | 32 | ### Versioning and Deprecation Policy 33 | * [Mastercard Versioning and Deprecation Policy](https://github.com/Mastercard/.github/blob/main/CLIENT_LIBRARY_DEPRECATION_POLICY.md) 34 | 35 | ## Usage 36 | ### Prerequisites 37 | Before using this library, you will need to set up a project in the [Mastercard Developers Portal](https://developer.mastercard.com). 38 | 39 | As part of this set up, you'll receive credentials for your app: 40 | * A consumer key (displayed on the Mastercard Developer Portal) 41 | * A private request signing key (matching the public certificate displayed on the Mastercard Developer Portal) 42 | 43 | ### Adding the Library to Your Project 44 | 45 | ```shell 46 | composer require mastercard/oauth1-signer 47 | ``` 48 | 49 | ### Loading the Signing Key 50 | 51 | A private key object can be created by calling the `AuthenticationUtils::loadSigningKey` function: 52 | 53 | ```php 54 | use Mastercard\Developer\OAuth\Utils\AuthenticationUtils; 55 | // … 56 | $signingKey = AuthenticationUtils::loadSigningKey( 57 | '', 58 | '', 59 | ''); 60 | ``` 61 | 62 | ### Creating the OAuth Authorization Header 63 | The method that does all the heavy lifting is `OAuth::getAuthorizationHeader`. You can call into it directly and as long as you provide the correct parameters, it will return a string that you can add into your request's `Authorization` header. 64 | 65 | ```php 66 | use Mastercard\Developer\OAuth\OAuth; 67 | // … 68 | $consumerKey = ''; 69 | $uri = 'https://sandbox.api.mastercard.com/service'; 70 | $method = 'POST'; 71 | $payload = 'Hello world!'; 72 | $authHeader = OAuth::getAuthorizationHeader($uri, $method, $payload, $consumerKey, $signingKey); 73 | ``` 74 | 75 | ### Signing HTTP Client Request Objects 76 | 77 | Alternatively, you can use helper classes for some of the commonly used HTTP clients. 78 | 79 | These classes, provided in the `Mastercard\Developer\Signers\` namespace, will modify the provided request object in-place and will add the correct `Authorization` header. Once instantiated with a consumer key and private key, these objects can be reused. 80 | 81 | Usage briefly described below, but you can also refer to the test namespace for examples. 82 | 83 | + [cURL](#curl) 84 | + [GuzzleHttp](#guzzlehttp) 85 | 86 | #### cURL 87 | 88 | ##### POST example 89 | 90 | ```php 91 | use Mastercard\Developer\Signers\CurlRequestSigner; 92 | // … 93 | $method = 'POST'; 94 | $uri = 'https://sandbox.api.mastercard.com/service'; 95 | $payload = json_encode(['foo' => 'bår']); 96 | $headers = array( 97 | 'Content-Type: application/json', 98 | 'Content-Length: ' . strlen($payload) 99 | ); 100 | $handle = curl_init($uri); 101 | curl_setopt_array($handle, array(CURLOPT_RETURNTRANSFER => 1, CURLOPT_CUSTOMREQUEST => $method, CURLOPT_POSTFIELDS => $payload)); 102 | $signer = new CurlRequestSigner($consumerKey, $signingKey); 103 | $signer->sign($handle, $method, $headers, $payload); 104 | $result = curl_exec($handle); 105 | curl_close($handle); 106 | ``` 107 | 108 | ##### GET example 109 | 110 | ```php 111 | use Mastercard\Developer\Signers\CurlRequestSigner; 112 | // … 113 | $method = 'GET'; 114 | $baseUri = 'https://sandbox.api.mastercard.com/service'; 115 | $queryParams = array('param1' => 'with spaces', 'param2' => 'encoded#symbol'); 116 | $uri = $baseUri . '?' . http_build_query($queryParams); 117 | $handle = curl_init($uri); 118 | curl_setopt_array($handle, array(CURLOPT_RETURNTRANSFER => 1)); 119 | $signer = new CurlRequestSigner($consumerKey, $signingKey); 120 | $signer->sign($handle, $method); 121 | $result = curl_exec($handle); 122 | curl_close($handle); 123 | ``` 124 | 125 | #### GuzzleHttp 126 | ```php 127 | use GuzzleHttp\Psr7\Request; 128 | use Mastercard\Developer\Signers\PsrHttpMessageSigner; 129 | // … 130 | $payload = '{"foo":"bår"}'; 131 | $headers = ['Content-Type' => 'application/json']; 132 | $request = new Request('POST', 'https://sandbox.api.mastercard.com/service', $headers, $payload); 133 | $signer = new PsrHttpMessageSigner($consumerKey, $signingKey); 134 | $signer.sign($request); 135 | ``` 136 | 137 | ### Integrating with OpenAPI Generator API Client Libraries 138 | 139 | [OpenAPI Generator](https://github.com/OpenAPITools/openapi-generator) generates API client libraries from [OpenAPI Specs](https://github.com/OAI/OpenAPI-Specification). 140 | It provides generators and library templates for supporting multiple languages and frameworks. 141 | 142 | This project provides you with classes you can use when configuring your API client. These classes will take care of adding the correct `Authorization` header before sending the request. 143 | 144 | Generators currently supported: 145 | + [php](#php) 146 | 147 | #### php 148 | 149 | ##### OpenAPI Generator 150 | 151 | Client libraries can be generated using the following command: 152 | ```shell 153 | openapi-generator-cli generate -i openapi-spec.yaml -g php -o out 154 | ``` 155 | See also: 156 | * [OpenAPI Generator CLI Installation](https://openapi-generator.tech/docs/installation/) 157 | * [CONFIG OPTIONS for php](https://github.com/OpenAPITools/openapi-generator/blob/master/docs/generators/php.md) 158 | 159 | ##### Usage of the `PsrHttpMessageSigner` 160 | 161 | ```php 162 | use GuzzleHttp; 163 | use OpenAPI\Client\Api\ServiceApi; 164 | use OpenAPI\Client\Configuration 165 | use Mastercard\Developer\Signers\PsrHttpMessageSigner; 166 | // … 167 | $stack = new GuzzleHttp\HandlerStack(); 168 | $stack->setHandler(new GuzzleHttp\Handler\CurlHandler()); 169 | $stack->push(GuzzleHttp\Middleware::mapRequest([new PsrHttpMessageSigner($consumerKey, $signingKey), 'sign'])); 170 | $options = ['handler' => $stack]; 171 | $client = new GuzzleHttp\Client($options); 172 | $config = new Configuration(); 173 | $config->setHost('https://sandbox.api.mastercard.com'); 174 | $serviceApi = new ServiceApi($client, $config); 175 | // … 176 | ``` 177 | -------------------------------------------------------------------------------- /src/Developer/OAuth/OAuth.php: -------------------------------------------------------------------------------- 1 | $value) { 49 | $result .= (strlen($result) == 0 ? 'OAuth ' : ','); 50 | $result .= $key . '="' . rawurlencode($value) . '"'; 51 | } 52 | return $result; 53 | } 54 | 55 | /** 56 | * Parse query parameters out of the URL. https://tools.ietf.org/html/rfc5849#section-3.4.1.3 57 | * @return array 58 | * @throws \InvalidArgumentException 59 | */ 60 | private static function extractQueryParams($uri) { 61 | $uriParts = parse_url($uri); 62 | if (!$uriParts) { 63 | throw new \InvalidArgumentException('URI is not valid!'); 64 | } 65 | 66 | if (!array_key_exists('host', $uriParts)) { 67 | throw new \InvalidArgumentException('No URI host!'); 68 | } 69 | 70 | if (!array_key_exists('query', $uriParts)) { 71 | return array(); 72 | } 73 | 74 | $rawQueryString = $uriParts['query']; 75 | $decodedQueryString = rawurldecode($rawQueryString); 76 | $mustEncode = $decodedQueryString != $rawQueryString; 77 | 78 | $queryParameters = []; 79 | $rawParams = explode('&', $rawQueryString); 80 | foreach ($rawParams as $index => $pair) { 81 | if (empty($pair)) { 82 | continue; 83 | } 84 | $index = strpos($pair, '='); 85 | $key = rawurldecode($index > 0 ? substr($pair, 0, $index) : $pair); 86 | $value = ($index > 0 && strlen($pair) > $index + 1) ? rawurldecode(substr($pair, $index + 1)) : ''; 87 | $encodedKey = $mustEncode ? rawurlencode($key) : $key; 88 | $encodedValue = $mustEncode ? rawurlencode($value) : $value; 89 | if (!array_key_exists($encodedKey, $queryParameters)) { 90 | $queryParameters[$encodedKey] = array(); 91 | } 92 | array_push($queryParameters[$encodedKey], $encodedValue); 93 | } 94 | 95 | return $queryParameters; 96 | } 97 | 98 | /** 99 | * Generates a hash based on request payload as per https://tools.ietf.org/id/draft-eaton-oauth-bodyhash-00.html. 100 | * "If the request does not have an entity body, the hash should be taken over the empty string". 101 | * @return string 102 | */ 103 | private static function getBodyHash($payload) { 104 | return base64_encode(hash('sha256', $payload, true)); 105 | } 106 | 107 | /** 108 | * Lexicographically sort all parameters and concatenate them into a string as per https://tools.ietf.org/html/rfc5849#section-3.4.1.3.2. 109 | * @return string 110 | */ 111 | private static function getOAuthParamString($queryParameters, $oauthParameters) { 112 | foreach ($oauthParameters as $key => $value) { 113 | $oauthParameters[$key] = array($value); 114 | } 115 | $allParameters = array_merge($queryParameters, $oauthParameters); 116 | ksort($allParameters, SORT_NATURAL); 117 | 118 | // Build the OAuth parameter string 119 | $parameterString = ''; 120 | foreach ($allParameters as $key => $values) { 121 | asort($values, SORT_NATURAL); // Keys with same name are sorted by their values 122 | foreach ($values as $value) { 123 | $parameterString .= (strlen($parameterString) == 0 ? '' : '&'); 124 | $parameterString .= $key . '=' . $value; 125 | } 126 | } 127 | return $parameterString; 128 | } 129 | 130 | /** 131 | * Normalizes the URL as per https://tools.ietf.org/html/rfc5849#section-3.4.1.2. 132 | * @return string 133 | * @throws \InvalidArgumentException 134 | */ 135 | private static function getBaseUriString($uriString) { 136 | $uriParts = parse_url($uriString); 137 | if (!$uriParts) { 138 | throw new \InvalidArgumentException('URI is not valid!'); 139 | } 140 | 141 | // Remove query and fragment 142 | $normalizedUrl = strtolower($uriParts['scheme']) . '://' . strtolower($uriParts['host']); 143 | 144 | if (array_key_exists('port', $uriParts)) { 145 | // Remove port if it matches the default for scheme 146 | $port = $uriParts['port']; 147 | if (!empty($port) && $port != 80 && $port != 443) { 148 | $normalizedUrl .= ':' . $port; 149 | } 150 | } 151 | 152 | $path = ''; 153 | if (array_key_exists('path', $uriParts)) { 154 | $path = $uriParts['path']; 155 | } 156 | if (empty($path)) { 157 | $path = '/'; 158 | } 159 | return $normalizedUrl . $path; 160 | } 161 | 162 | /** 163 | * Generate a valid signature base string as per https://tools.ietf.org/html/rfc5849#section-3.4.1. 164 | * @return string 165 | */ 166 | private static function getSignatureBaseString($baseUri, $httpMethod, $oauthParamString) { 167 | return strtoupper($httpMethod) // Uppercase HTTP method 168 | . '&' . rawurlencode($baseUri) // Base URI 169 | . '&' . rawurlencode($oauthParamString); // OAuth parameter string 170 | } 171 | 172 | /** 173 | * Signs the signature base string using an RSA private key. The methodology is described at 174 | * https://tools.ietf.org/html/rfc5849#section-3.4.3 but Mastercard uses the stronger SHA-256 algorithm 175 | * as a replacement for the described SHA1 which is no longer considered secure. 176 | * @return string 177 | */ 178 | private static function signSignatureBaseString($baseString, $privateKey) { 179 | openssl_sign($baseString, $signature, $privateKey, "SHA256"); 180 | return base64_encode($signature); 181 | } 182 | 183 | /** 184 | * Generates a random string for replay protection as per https://tools.ietf.org/html/rfc5849#section-3.3. 185 | * @return string 186 | */ 187 | private static function getNonce($length = 16) { 188 | $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; 189 | $charactersLength = strlen($characters); 190 | $randomString = ''; 191 | for ($i = 0; $i < $length; $i++) { 192 | $randomString .= $characters[rand(0, $charactersLength - 1)]; 193 | } 194 | return $randomString; 195 | } 196 | 197 | /** 198 | * Returns UNIX Timestamp as required per https://tools.ietf.org/html/rfc5849#section-3.3. 199 | * @return int 200 | */ 201 | private static function getTimestamp() { 202 | return time(); 203 | } 204 | } -------------------------------------------------------------------------------- /tests/Developer/OAuth/OAuthTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(4, sizeof($queryParams)); 48 | $this->assertSame(array('10'), $queryParams['length']); 49 | $this->assertSame(array('0', '1'), $queryParams['offset']); 50 | $this->assertSame(array(''), $queryParams['empty']); 51 | $this->assertSame(array(''), $queryParams['odd']); 52 | } 53 | 54 | public function testExtractQueryParams_ShouldSupportRfcExample() { 55 | 56 | // GIVEN 57 | $uri = 'https://example.com/request?b5=%3D%253D&a3=a&c%40=&a2=r%20b'; // See: https://tools.ietf.org/html/rfc5849#section-3.4.1.3.1 58 | 59 | // WHEN 60 | $queryParams = self::callExtractQueryParams($uri); 61 | 62 | // THEN 63 | $this->assertEquals(4, sizeof($queryParams)); 64 | $this->assertSame(array('%3D%253D'), $queryParams['b5']); 65 | $this->assertSame(array('a'), $queryParams['a3']); 66 | $this->assertSame(array(''), $queryParams['c%40']); 67 | $this->assertSame(array('r%20b'), $queryParams['a2']); 68 | } 69 | 70 | public function testExtractQueryParams_ShouldNotEncodeParams_WhenUriStringWithDecodedParams() { 71 | 72 | // GIVEN 73 | $uri = 'https://example.com/request?colon=:&plus=+&comma=,'; 74 | 75 | // WHEN 76 | $queryParams = self::callExtractQueryParams($uri); 77 | 78 | // THEN 79 | $this->assertEquals(3, sizeof($queryParams)); 80 | $this->assertSame(array(':'), $queryParams['colon']); 81 | $this->assertSame(array('+'), $queryParams['plus']); 82 | $this->assertSame(array(','), $queryParams['comma']); 83 | } 84 | 85 | public function testExtractQueryParams_ShouldEncodeParams_WhenUriStringWithEncodedParams() { 86 | 87 | // GIVEN 88 | $uri = 'https://example.com/request?colon=%3A&plus=%2B&comma=%2C'; 89 | 90 | // WHEN 91 | $queryParams = self::callExtractQueryParams($uri); 92 | 93 | // THEN 94 | $this->assertEquals(3, sizeof($queryParams)); 95 | $this->assertSame(array('%3A'), $queryParams['colon']); 96 | $this->assertSame(array('%2B'), $queryParams['plus']); 97 | $this->assertSame(array('%2C'), $queryParams['comma']); 98 | } 99 | 100 | public function testParameterEncoding_ShouldCreateExpectedSignatureBaseString_WhenQueryParamsEncodedInUri() { 101 | 102 | // GIVEN 103 | $uri = 'https://example.com/?param=token1%3Atoken2'; 104 | 105 | // WHEN 106 | $queryParams = self::callExtractQueryParams($uri); 107 | $paramString = self::callGetOAuthParamString(array($queryParams, array())); 108 | $baseString = self::callGetSignatureBaseString(array('https://example.com', 'GET', $paramString)); 109 | 110 | // THEN 111 | $this->assertEquals('GET&https%3A%2F%2Fexample.com¶m%3Dtoken1%253Atoken2', $baseString); 112 | } 113 | 114 | public function testParameterEncoding_ShouldCreateExpectedSignatureBaseString_WhenQueryParamsNotEncodedInUri() { 115 | 116 | // GIVEN 117 | $uri = 'https://example.com/?param=token1:token2'; 118 | 119 | // WHEN 120 | $queryParams = self::callExtractQueryParams($uri); 121 | $paramString = self::callGetOAuthParamString(array($queryParams, array())); 122 | $baseString = self::callGetSignatureBaseString(array('https://example.com', 'GET', $paramString)); 123 | 124 | // THEN 125 | $this->assertEquals('GET&https%3A%2F%2Fexample.com¶m%3Dtoken1%3Atoken2', $baseString); 126 | } 127 | 128 | public function testGetBodyHash() { 129 | $this->assertEquals('47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=', self::callGetBodyHash(NULL)); 130 | $this->assertEquals('47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=', self::callGetBodyHash('')); 131 | $this->assertEquals('+Z+PWW2TJDnPvRcTgol+nKO3LT7xm8smnsg+//XMIyI=', self::callGetBodyHash('{"foõ":"bar"}')); 132 | } 133 | 134 | public function testGetOAuthParamString_ShouldSupportRfcExample() { 135 | $queryParameters = array(); 136 | $queryParameters['b5'] = array('%3D%253D'); 137 | $queryParameters['a3'] = array('a', '2%20q'); 138 | $queryParameters['c%40'] = array(''); 139 | $queryParameters['a2'] = array('r%20b'); 140 | $queryParameters['c2'] = array(''); 141 | $oauthParameters = array(); 142 | $oauthParameters['oauth_consumer_key'] = '9djdj82h48djs9d2'; 143 | $oauthParameters['oauth_token'] = 'kkk9d7dh3k39sjv7'; 144 | $oauthParameters['oauth_signature_method'] = 'HMAC-SHA1'; 145 | $oauthParameters['oauth_timestamp'] = '137131201'; 146 | $oauthParameters['oauth_nonce'] = '7d8f3e4a'; 147 | 148 | $paramString = self::callGetOAuthParamString(array($queryParameters, $oauthParameters)); 149 | $this->assertEquals('a2=r%20b&a3=2%20q&a3=a&b5=%3D%253D&c%40=&c2=&oauth_consumer_key=9djdj82h48djs9d2&oauth_nonce=7d8f3e4a&oauth_signature_method=HMAC-SHA1&oauth_timestamp=137131201&oauth_token=kkk9d7dh3k39sjv7', $paramString); 150 | } 151 | 152 | public function testGetOAuthParamString_ShouldUseAscendingByteValueOrdering() { 153 | $queryParameters = array(); 154 | $queryParameters['b'] = array('b'); 155 | $queryParameters['A'] = array('a', 'A'); 156 | $queryParameters['B'] = array('B'); 157 | $queryParameters['a'] = array('A', 'a'); 158 | $queryParameters['0'] = array('0'); 159 | $oauthParameters = array(); 160 | 161 | $paramString = self::callGetOAuthParamString(array($queryParameters, $oauthParameters)); 162 | $this->assertEquals('0=0&A=A&A=a&B=B&a=A&a=a&b=b', $paramString); 163 | } 164 | 165 | public function testGetBaseUriString_ShouldSupportRfcExamples() { 166 | $this->assertEquals('https://www.example.net:8080/', self::callGetBaseUriString('https://www.example.net:8080')); 167 | $this->assertEquals('http://example.com/r%20v/X', self::callGetBaseUriString('http://EXAMPLE.COM:80/r%20v/X?id=123')); 168 | } 169 | 170 | public function testGetBaseUriString_ShouldRemoveRedundantPorts() { 171 | $this->assertEquals('https://api.mastercard.com/test', self::callGetBaseUriString('https://api.mastercard.com:443/test?query=param')); 172 | $this->assertEquals('http://api.mastercard.com/test', self::callGetBaseUriString('http://api.mastercard.com:80/test')); 173 | $this->assertEquals('https://api.mastercard.com:17443/test', self::callGetBaseUriString('https://api.mastercard.com:17443/test?query=param')); 174 | } 175 | 176 | public function testGetBaseUriString_ShouldRemoveFragments() { 177 | $this->assertEquals('https://api.mastercard.com/test', self::callGetBaseUriString('https://api.mastercard.com/test?query=param#fragment')); 178 | } 179 | 180 | public function testGetBaseUriString_ShouldAddTrailingSlash() { 181 | $this->assertEquals('https://api.mastercard.com/', self::callGetBaseUriString('https://api.mastercard.com')); 182 | } 183 | 184 | public function testGetBaseUriString_ShouldUseLowercaseSchemesAndHosts() { 185 | $this->assertEquals('https://api.mastercard.com/TEST', self::callGetBaseUriString('HTTPS://API.MASTERCARD.COM/TEST')); 186 | } 187 | 188 | public function testGetSignatureBaseString_Nominal() { 189 | $method = 'POST'; 190 | $queryParameters = array(); 191 | $queryParameters['param2'] = array('hello'); 192 | $queryParameters['first_param'] = array('value', 'othervalue'); 193 | $oauthParameters = array(); 194 | $oauthParameters['oauth_nonce'] = 'randomnonce'; 195 | $oauthParameters['oauth_body_hash'] = 'body/hash'; 196 | $oauthParamString = self::callGetOAuthParamString(array($queryParameters, $oauthParameters)); 197 | $actualSignatureBaseString = self::callGetSignatureBaseString(array('https://api.mastercard.com', $method, $oauthParamString)); 198 | $expectedSignatureBaseString = 'POST&https%3A%2F%2Fapi.mastercard.com&first_param%3Dothervalue%26first_param%3Dvalue%26oauth_body_hash%3Dbody%2Fhash%26oauth_nonce%3Drandomnonce%26param2%3Dhello'; 199 | $this->assertEquals($expectedSignatureBaseString, $actualSignatureBaseString); 200 | } 201 | 202 | public function testSignSignatureBaseString() { 203 | $expectedSignatureString = 'IJeNKYGfUhFtj5OAPRI92uwfjJJLCej3RCMLbp7R6OIYJhtwxnTkloHQ2bgV7fks4GT/A7rkqrgUGk0ewbwIC6nS3piJHyKVc7rvQXZuCQeeeQpFzLRiH3rsb+ZS+AULK+jzDje4Fb+BQR6XmxuuJmY6YrAKkj13Ln4K6bZJlSxOizbNvt+Htnx+hNd4VgaVBeJKcLhHfZbWQxK76nMnjY7nDcM/2R6LUIR2oLG1L9m55WP3bakAvmOr392ulv1+mWCwDAZZzQ4lakDD2BTu0ZaVsvBW+mcKFxYeTq7SyTQMM4lEwFPJ6RLc8jJJ+veJXHekLVzWg4qHRtzNBLz1mA=='; 204 | $this->assertEquals($expectedSignatureString, self::callSignSignatureBaseString(array('baseString', TestUtils::getTestSigningKey()))); 205 | } 206 | 207 | public function testGetSignatureBaseString_Integrated() { 208 | $body = '19961TESTTEST55555555551234567890
5555 Test LaneTESTXX12345USA
JohnSmith12345678905555555555
5555 Test LaneTESTXX12345USA
1234567890XX
'; 209 | $method = 'POST'; 210 | $uri = 'https://sandbox.api.mastercard.com/fraud/merchant/v1/termination-inquiry?Format=XML&PageOffset=0&PageLength=10'; 211 | $queryParameters = self::callExtractQueryParams($uri); 212 | $oauthParameters = array(); 213 | $oauthParameters['oauth_consumer_key'] = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; 214 | $oauthParameters['oauth_nonce'] = '1111111111111111111'; 215 | $oauthParameters['oauth_signature_method'] = 'RSA-SHA256'; 216 | $oauthParameters['oauth_timestamp'] = '1111111111'; 217 | $oauthParameters['oauth_version'] = '1.0'; 218 | $oauthParameters['oauth_body_hash'] = self::callGetBodyHash($body); 219 | 220 | $oauthParamString = self::callGetOAuthParamString(array($queryParameters, $oauthParameters)); 221 | $actualSignatureBaseString = self::callGetSignatureBaseString(array(self::callGetBaseUriString($uri), $method, $oauthParamString)); 222 | $expectedSignatureBaseString = 'POST&https%3A%2F%2Fsandbox.api.mastercard.com%2Ffraud%2Fmerchant%2Fv1%2Ftermination-inquiry&Format%3DXML%26PageLength%3D10%26PageOffset%3D0%26oauth_body_hash%3Dh2Pd7zlzEZjZVIKB4j94UZn%2FxxoR3RoCjYQ9%2FJdadGQ%3D%26oauth_consumer_key%3Dxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx%26oauth_nonce%3D1111111111111111111%26oauth_signature_method%3DRSA-SHA256%26oauth_timestamp%3D1111111111%26oauth_version%3D1.0'; 223 | $this->assertEquals($expectedSignatureBaseString, $actualSignatureBaseString); 224 | } 225 | 226 | public function testRawUrlEncode() { 227 | $this->assertEquals('Format%3DXML', rawurlencode('Format=XML')); 228 | $this->assertEquals('WhqqH%2BTU95VgZMItpdq78BWb4cE%3D', rawurlencode('WhqqH+TU95VgZMItpdq78BWb4cE=')); 229 | $this->assertEquals('WhqqH%2BTU95VgZMItpdq78BWb4cE%3D%26o', rawurlencode('WhqqH+TU95VgZMItpdq78BWb4cE=&o')); 230 | $this->assertEquals('WhqqH%2BTU95VgZ~Itpdq78BWb4cE%3D%26o', rawurlencode('WhqqH+TU95VgZ~Itpdq78BWb4cE=&o')); // Tilde stays unescaped 231 | } 232 | 233 | public function testGetNonce_ShouldHaveLengthOf16() { 234 | $nonce = self::callGetNonce(array()); 235 | $this->assertEquals(16, strlen($nonce)); 236 | } 237 | 238 | } 239 | --------------------------------------------------------------------------------