├── .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/)
3 |
4 | [](https://github.com/Mastercard/.github/blob/main/CLIENT_LIBRARY_DEPRECATION_POLICY.md)
5 | [](https://sonarcloud.io/dashboard?id=Mastercard_oauth1-signer-php)
6 | [](https://packagist.org/packages/mastercard/oauth1-signer)
7 | [](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 = '19961TESTTEST555555555512345678905555 Test LaneTESTXX12345USAJohnSmith123456789055555555555555 Test LaneTESTXX12345USA1234567890XX';
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 |
--------------------------------------------------------------------------------