├── AUTHORS ├── CONTRIBUTORS ├── LICENSE ├── README.md ├── composer.json ├── src ├── DownloadIterator.php ├── GitkitAccount.php ├── GitkitClient.php ├── GitkitClientException.php ├── GitkitServerException.php └── RpcHelper.php └── tests ├── AllTests.php ├── GitkitClientTest.php ├── RpcHelperTest.php ├── TestData.php └── TestKey.p12 /AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the official list of Google Identity Toolkit client library authors for copyright purposes. 2 | # This file is distinct from the CONTRIBUTORS files. 3 | # See the latter for an explanation. 4 | 5 | # Names should be added to this file as: 6 | # Name or Organization 7 | # The email address is not required for organizations. 8 | 9 | Google Inc. 10 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | # People who have agreed to one of the CLAs and can contribute patches. 2 | # The AUTHORS file lists the copyright holders; this file 3 | # lists people. For example, Google employees are listed here 4 | # but not in AUTHORS, because Google holds the copyright. 5 | # 6 | # https://developers.google.com/open-source/cla/individual 7 | # https://developers.google.com/open-source/cla/corporate 8 | # 9 | # Names should be added to this file as: 10 | # Name 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2014 Google Inc. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Google Identity Toolkit client library for PHP 2 | 3 | This is the PHP client library for Google Identity Toolkit services. 4 | 5 | ## Example Code 6 | 7 | ```php 8 | require_once __DIR__ . '/vendor/autoload.php'; 9 | 10 | $gitkitClient = Gitkit_Client::createFromFile("gitkit-server-config.json"); 11 | 12 | // ---- upload account ----- 13 | $hashKey = "\x01\x02\x03"; 14 | $gitkitClient->uploadUsers('HMAC_SHA1', $hashKey, createNewUsers($hashKey)); 15 | 16 | // --- verify gitkit token ---- 17 | $user = $gitkitClient->validateToken("eyJhb..."); 18 | 19 | // ---- get account info by user identifier ---- 20 | $user = $gitkitClient->getUserById("1234"); 21 | 22 | // ---- get a url to send to user's email address to verify ownership ----- 23 | $verificationLink = $gitkitClient->getEmailVerificationLink("1234@example.com"); 24 | 25 | // ---- download account ---- 26 | $iterator = $gitkitClient->getAllUsers(3); 27 | while ($iterator->valid()) { 28 | $user = $iterator->current(); 29 | // $user is a Gitkit_Account object 30 | $iterator->next(); 31 | } 32 | 33 | // ---- delete account ---- 34 | $gitkitClient->deleteUser('1234'); 35 | 36 | function createNewUsers($hashKey) { 37 | $allUsers = array(); 38 | 39 | $gitkitUser = new Gitkit_Account(); 40 | $gitkitUser->setEmail("1234@example.com"); 41 | $gitkitUser->setUserId("1234"); 42 | $salt = "\05\06\07"; 43 | $password = '1111'; 44 | $gitkitUser->setSalt($salt); 45 | $gitkitUser->setPasswordHash(hash_hmac('sha1', $password . $salt, $hashKey, true)); 46 | array_push($allUsers, $gitkitUser); 47 | 48 | $gitkitUser = new Gitkit_Account(); 49 | $gitkitUser->setEmail('5678@example.com'); 50 | $gitkitUser->setUserId('5678'); 51 | $salt = "\15\16\17"; 52 | $password = '5555'; 53 | $gitkitUser->setSalt($salt); 54 | $gitkitUser->setPasswordHash(hash_hmac('sha1', $password . $salt, $hashKey, true)); 55 | array_push($allUsers, $gitkitUser); 56 | 57 | return $allUsers; 58 | } 59 | ``` -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "google/identity-toolkit-php-client", 3 | "type": "library", 4 | "version": "1.0.8", 5 | "description": "Client library for Google Identity Toolkit APIs", 6 | "keywords": ["gitkit"], 7 | "homepage": "https://developers.google.com/identity-toolkit/", 8 | "license": "Apache-2.0", 9 | "require": { 10 | "php": ">=5.2.1", 11 | "google/apiclient": "~1.0" 12 | }, 13 | "require-dev": { 14 | "phpunit/phpunit": "4.1.*" 15 | }, 16 | "autoload": { 17 | "classmap": [ 18 | "src/" 19 | ] 20 | }, 21 | "include-path": ["src/"] 22 | } 23 | -------------------------------------------------------------------------------- /src/DownloadIterator.php: -------------------------------------------------------------------------------- 1 | rpcHelper = $rpcHelper; 34 | $this->maxResults = $maxResults; 35 | $this->isLastPage = false; 36 | $this->getPaginatedResults(); 37 | } 38 | 39 | public function valid() { 40 | if (!$this->iterator->valid()) { 41 | $this->getPaginatedResults(); 42 | } 43 | return $this->iterator->valid(); 44 | } 45 | 46 | public function next() { 47 | if ($this->iterator->valid()) { 48 | $this->iterator->next(); 49 | } else { 50 | throw new Gitkit_ClientException("invalid download account iterator"); 51 | } 52 | } 53 | 54 | public function current() { 55 | if ($this->iterator->valid()) { 56 | return new Gitkit_Account($this->iterator->current()); 57 | } else { 58 | throw new Gitkit_ClientException("invalid download account iterator"); 59 | } 60 | } 61 | 62 | private function getPaginatedResults() { 63 | if ($this->isLastPage) { 64 | $this->iterator = new EmptyIterator(); 65 | } else { 66 | $response = $this->rpcHelper->downloadAccount($this->nextPageToken, 67 | $this->maxResults); 68 | if (isset($response['nextPageToken'])) { 69 | $this->nextPageToken = $response['nextPageToken']; 70 | } else { 71 | $this->isLastPage = true; 72 | } 73 | if (isset($response['users'])) { 74 | $arr = new ArrayObject($response['users']); 75 | $this->iterator = new NoRewindIterator($arr->getIterator()); 76 | } else { 77 | $this->iterator = new EmptyIterator(); 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/GitkitAccount.php: -------------------------------------------------------------------------------- 1 | localId = $apiResponse['localId']; 35 | } else if (isset($apiResponse['user_id'])) { 36 | $this->localId = $apiResponse['user_id']; 37 | } 38 | if (isset($apiResponse['email'])) { 39 | $this->email = $apiResponse['email']; 40 | } 41 | if (isset($apiResponse['displayName'])) { 42 | $this->displayName = $apiResponse['displayName']; 43 | } 44 | if (isset($apiResponse['photoUrl'])) { 45 | $this->photoUrl = $apiResponse['photoUrl']; 46 | } 47 | if (isset($apiResponse['emailVerified'])) { 48 | $this->emailVerified = $apiResponse['emailVerified']; 49 | } 50 | if (isset($apiResponse['providerUserInfo'])) { 51 | $this->providerInfo = $apiResponse['providerUserInfo']; 52 | } 53 | } 54 | 55 | public function getUserId() { 56 | return $this->localId; 57 | } 58 | 59 | public function setUserId($localId) { 60 | $this->localId = $localId; 61 | } 62 | 63 | public function getEmail() { 64 | return $this->email; 65 | } 66 | 67 | public function setEmail($email) { 68 | $this->email = $email; 69 | } 70 | 71 | public function getProviderId() { 72 | return $this->providerId; 73 | } 74 | 75 | public function setProviderId($providerId) { 76 | $this->providerId = $providerId; 77 | } 78 | 79 | public function getProviderInfo() { 80 | return $this->providerInfo; 81 | } 82 | 83 | public function getDisplayName() { 84 | return $this->displayName; 85 | } 86 | 87 | public function setDisplayName($displayName) { 88 | $this->displayName = $displayName; 89 | } 90 | 91 | public function getPhotoUrl() { 92 | return $this->photoUrl; 93 | } 94 | 95 | public function setPhotoUrl($photoUrl) { 96 | $this->photoUrl = $photoUrl; 97 | } 98 | 99 | public function isEmailVerified() { 100 | return $this->emailVerified; 101 | } 102 | 103 | public function setEmailVerified($verified) { 104 | $this->emailVerified = $verified; 105 | } 106 | 107 | public function getPasswordHash() { 108 | return $this->passwordHash; 109 | } 110 | 111 | public function setPasswordHash($passwordHash) { 112 | $this->passwordHash = $passwordHash; 113 | } 114 | 115 | public function getSalt() { 116 | return $this->salt; 117 | } 118 | 119 | public function setSalt($salt) { 120 | $this->salt = $salt; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/GitkitClient.php: -------------------------------------------------------------------------------- 1 | clientId = $clientId; 48 | $this->widgetUrl = $widgetUrl; 49 | $this->cookieName = $cookieName; 50 | $this->oauth2Client = new Google_Auth_OAuth2(new Google_Client()); 51 | $this->rpcHelper = $rpcHelper; 52 | $this->projectId = $projectId; 53 | } 54 | 55 | /** 56 | * Creates a Gitkit client from a config file (json format). 57 | * 58 | * @param string $file file name of the json config file 59 | * @return Gitkit_Client created Gitkit client 60 | */ 61 | public static function createFromFile($file) { 62 | $jsonConfig = json_decode(file_get_contents($file), true); 63 | return self::createFromConfig($jsonConfig); 64 | } 65 | 66 | /** 67 | * Creates a Gitkit client from the config array. 68 | * 69 | * @param array $config config parameters 70 | * @param null|Gitkit_RpcHelper $rpcHelper Gitkit Rpc helper object 71 | * @return Gitkit_Client created Gitkit client 72 | * @throws Gitkit_ClientException if required config is missing 73 | */ 74 | public static function createFromConfig($config, $rpcHelper = null) { 75 | $clientId = $config['clientId']; 76 | $projectId = $config['projectId']; 77 | if (!isset($clientId) && !isset($projectId)) { 78 | throw new Gitkit_ClientException("Missing projectId or clientId in server configuration."); 79 | } 80 | if (!isset($config['widgetUrl'])) { 81 | throw new Gitkit_ClientException("\"widgetUrl\" should be configured"); 82 | } 83 | if (isset($config["cookieName"])) { 84 | $cookieName = $config['cookieName']; 85 | } else { 86 | $cookieName = self::$DEFAULT_COOKIE_NAME; 87 | } 88 | if (!$rpcHelper) { 89 | if (!isset($config['serviceAccountEmail'])) { 90 | throw new Gitkit_ClientException( 91 | "\"serviceAccountEmail\" should be configured"); 92 | } 93 | if (!isset($config['serviceAccountPrivateKeyFile'])) { 94 | throw new Gitkit_ClientException( 95 | "\"serviceAccountPrivateKeyFile\" should be configured"); 96 | } 97 | $p12Key = file_get_contents($config["serviceAccountPrivateKeyFile"]); 98 | if ($p12Key === false) { 99 | throw new Gitkit_ClientException( 100 | "Can not read file " . $config["serviceAccountPrivateKeyFile"]); 101 | } 102 | if (isset($config['serverApiKey'])) { 103 | $serverApiKey = $config['serverApiKey']; 104 | } else { 105 | $serverApiKey = null; 106 | } 107 | $rpcHelper = new Gitkit_RpcHelper( 108 | $config["serviceAccountEmail"], 109 | $p12Key, 110 | self::$GITKIT_API_BASE, 111 | new Google_Auth_OAuth2(new Google_Client()), 112 | $serverApiKey); 113 | } 114 | return new Gitkit_Client($clientId, $config['widgetUrl'], 115 | $cookieName, $rpcHelper, $projectId); 116 | } 117 | 118 | /** 119 | * Validates a Gitkit token. User info is extracted from the token only. 120 | * 121 | * @param string $gitToken token to be checked 122 | * @return Gitkit_Account|null Gitkit user corresponding to the token, null 123 | * for invalid token 124 | */ 125 | public function validateToken($gitToken) { 126 | if ($gitToken) { 127 | $loginTicket = null; 128 | $auds = array_filter( 129 | array($this->projectId, $this->clientId), 130 | function($x) { 131 | return isset($x); 132 | }); 133 | foreach ($auds as $aud) { 134 | try { 135 | $loginTicket = $this->oauth2Client->verifySignedJwtWithCerts( 136 | $gitToken, 137 | $this->getCerts(), 138 | $aud, 139 | self::$GTIKIT_TOKEN_ISSUER, 140 | 180 * 86400)->getAttributes(); 141 | break; 142 | } catch (Google_Auth_Exception $e) { 143 | if (strpos($e->getMessage(), "Wrong recipient") === false) { 144 | throw $e; 145 | } 146 | } 147 | } 148 | if (!isset($loginTicket)) { 149 | throw new Google_Auth_Exception( 150 | "Gitkit token audience doesn't match projectId or clientId in server configuration."); 151 | } 152 | $jwt = $loginTicket["payload"]; 153 | if ($jwt) { 154 | $user = new Gitkit_Account(); 155 | $user->setUserId($jwt["user_id"]); 156 | $user->setEmail($jwt["email"]); 157 | if (isset($jwt["provider_id"])) { 158 | $user->setProviderId($jwt["provider_id"]); 159 | } else { 160 | $user->setProviderId(null); 161 | } 162 | $user->setEmailVerified($jwt["verified"]); 163 | if (isset($jwt["display_name"])) { 164 | $user->setDisplayName($jwt["display_name"]); 165 | } 166 | if (isset($jwt["photo_url"])) { 167 | $user->setPhotoUrl($jwt["photo_url"]); 168 | } 169 | return $user; 170 | } 171 | } 172 | return null; 173 | } 174 | 175 | /** 176 | * Validates the token in the http request cookie 177 | * 178 | * @return Gitkit_Account|null Gitkit user corresponding to the token, null 179 | * for invalid token 180 | */ 181 | public function validateTokenInRequest() { 182 | return $this->validateToken($_COOKIE[$this->cookieName], $this->clientId); 183 | } 184 | 185 | /** 186 | * Gets raw token string in the http request. 187 | * 188 | * @return mixed token string 189 | */ 190 | public function getTokenString() { 191 | if (isset($_COOKIE[$this->cookieName])) { 192 | return $_COOKIE[$this->cookieName]; 193 | } 194 | return null; 195 | } 196 | 197 | /** 198 | * Gets GitkitUser for the http request. Complete user info is retrieved from 199 | * Gitkit server. 200 | * 201 | * @return Gitkit_Account|null Gitkit user at Gitkit server, null for invalid 202 | * token 203 | */ 204 | public function getUserInRequest() { 205 | if (isset($_COOKIE[$this->cookieName])) { 206 | $user = $this->validateToken($_COOKIE[$this->cookieName], 207 | $this->clientId); 208 | if ($user) { 209 | $accountInfo = $this->getUserById($user->getUserId()); 210 | $accountInfo->setProviderId($user->getProviderId()); 211 | return $accountInfo; 212 | } 213 | } 214 | return null; 215 | } 216 | 217 | /** 218 | * Gets user info by email. 219 | * 220 | * @param string $email user email 221 | * @return Gitkit_Account user account info 222 | */ 223 | public function getUserByEmail($email) { 224 | return new Gitkit_Account($this->rpcHelper->getAccountInfoByEmail($email)); 225 | } 226 | 227 | /** 228 | * Gets user info by user identifier at Gitkit. 229 | * 230 | * @param string $id user identifier 231 | * @return Gitkit_Account user account info 232 | */ 233 | public function getUserById($id) { 234 | return new Gitkit_Account($this->rpcHelper->getAccountInfoById($id)); 235 | } 236 | 237 | /** 238 | * Sets user info at Gitkit server. 239 | * 240 | * @param Gitkit_Account $gitkitAccount user info to be updated 241 | * @return mixed server response 242 | */ 243 | public function updateUser($gitkitAccount) { 244 | return $this->rpcHelper->updateAccount($gitkitAccount); 245 | } 246 | 247 | /** 248 | * Deletes a user account at Gitkit server. 249 | * 250 | * @param string $id user identifier to be deleted 251 | * @return mixed server response 252 | */ 253 | public function deleteUser($id) { 254 | return $this->rpcHelper->deleteAccount($id); 255 | } 256 | 257 | /** 258 | * Uploads multiple accounts info to Gitkit server. 259 | * 260 | * @param string $hashAlgorithm password hash algorithm. See Gitkit doc for 261 | * supported names. 262 | * @param string $hashKey raw key for the algorithm 263 | * @param array $accounts array of Gitkit_Account to be uploaded 264 | * @param null|int $rounds Rounds of the hash function 265 | * @param null|int $memoryCost Memory cost of the hash function 266 | * @throws Gitkit_ServerException if error happens 267 | */ 268 | public function uploadUsers($hashAlgorithm, $hashKey, $accounts, 269 | $rounds = null, $memoryCost = null) { 270 | $this->rpcHelper->uploadAccount($hashAlgorithm, $hashKey, 271 | $this->toJsonRequest($accounts), $rounds, $memoryCost); 272 | } 273 | 274 | /** 275 | * Downloads all user account from Gitkit server. 276 | * 277 | * Usage: 278 | * $iterator = $gitkitClient->getAllUsers(10); 279 | * while ($iterator->valid()) { 280 | * // do something with ($iterator->current()); 281 | * $iterator->next(); 282 | * } 283 | * 284 | * @param int $maxResults max results per request 285 | * @return Gitkit_DownloadIterator iterator to fetch all user accounts 286 | */ 287 | public function getAllUsers($maxResults = null) { 288 | return new Gitkit_DownloadIterator($this->rpcHelper, $maxResults); 289 | } 290 | 291 | /** 292 | * Gets out-of-band results for ResetPassword, ChangeEmail operations etc. 293 | * 294 | * @param null|array $param http post body 295 | * @param null|string $user_ip end user IP address 296 | * @param null|string $gitkit_token Gitkit token in the request 297 | * @return array out-of-band results: 298 | * array( 299 | * 'email' => email of the user, 300 | * 'oldEmail' => old email (for ChangeEmail only), 301 | * 'newEmail' => new email (for ChangeEmail only), 302 | * 'oobLink' => url for user click to finish the operation, 303 | * 'action' => 'RESET_PASSWORD', or 'CHANGE_EMAIL', 304 | * 'response_body' => http response to be sent back to Gitkit widget 305 | * ) 306 | */ 307 | public function getOobResults($param = null, 308 | $user_ip = null, $gitkit_token = null) { 309 | if (!$param) { 310 | $param = $_POST; 311 | } 312 | if (!$user_ip) { 313 | $user_ip = $_SERVER['REMOTE_ADDR']; 314 | } 315 | if (!$gitkit_token) { 316 | $gitkit_token = $this->getTokenString(); 317 | } 318 | if (isset($param['action'])) { 319 | try { 320 | if ($param['action'] == 'resetPassword') { 321 | $oob_link = $this->buildOobLink( 322 | $this->passwordResetRequest($param, $user_ip), 323 | $param['action']); 324 | return $this->passwordResetResponse($oob_link, $param); 325 | } else if ($param['action'] == 'changeEmail') { 326 | if (!$gitkit_token) { 327 | return $this->failureOobMsg('login is required'); 328 | } 329 | $oob_link = $this->buildOobLink( 330 | $this->changeEmailRequest($param, $user_ip, $gitkit_token), 331 | $param['action']); 332 | return $this->emailChangeResponse($oob_link, $param); 333 | } 334 | } catch (Gitkit_ClientException $error) { 335 | return $this->failureOobMsg($error->getMessage()); 336 | } 337 | } 338 | return $this->failureOobMsg('unknown action type'); 339 | } 340 | 341 | /** 342 | * Gets verification url to verify user's email. 343 | * 344 | * @param string $email user's email to be verified 345 | * @return string url for user click to verify the email 346 | */ 347 | public function getEmailVerificationLink($email) { 348 | $param = array( 349 | 'email' => $email, 350 | 'requestType' => 'VERIFY_EMAIL' 351 | ); 352 | return $this->buildOobLink($param, 'verifyEmail'); 353 | } 354 | 355 | /** 356 | * Gets Gitkit public certs. 357 | * 358 | * @return array certs in the format of {keyId => cert}. 359 | */ 360 | protected function getCerts() { 361 | return $this->rpcHelper->getGitkitCerts(); 362 | } 363 | 364 | /** 365 | * Converts Gitkit account array to json request. 366 | * 367 | * @param array $accounts Gitkit account array 368 | * @return array json request 369 | */ 370 | private function toJsonRequest($accounts) { 371 | $jsonUsers = array(); 372 | foreach($accounts as $account) { 373 | $user = array( 374 | 'email' => $account->getEmail(), 375 | 'localId' => $account->getUserId(), 376 | 'emailVerified' => $account->isEmailVerified(), 377 | 'displayName' => $account->getDisplayName(), 378 | 'passwordHash' => Google_Utils::urlSafeB64Encode( 379 | $account->getPasswordHash()), 380 | 'salt' => Google_Utils::urlSafeB64Encode($account->getSalt()) 381 | ); 382 | array_push($jsonUsers, $user); 383 | } 384 | return $jsonUsers; 385 | } 386 | 387 | /** 388 | * Builds the url of out-of-band confirmation. 389 | * 390 | * @param array $param oob request param 391 | * @param string $action 'RESET_PASSWORD' or 'CHANGE_EMAIL' 392 | * @return string the oob url 393 | */ 394 | private function buildOobLink($param, $action) { 395 | $code = $this->rpcHelper->getOobCode($param); 396 | $separator = parse_url($this->widgetUrl, PHP_URL_QUERY) ? '&' : '?'; 397 | return $this->widgetUrl . $separator . 398 | http_build_query(array('mode' => $action, 'oobCode' => $code)); 399 | } 400 | 401 | private function passwordResetRequest($param, $user_ip) { 402 | return array( 403 | 'email' => $param['email'], 404 | 'userIp' => $user_ip, 405 | 'captchaResp' => $param['response'], 406 | 'requestType' => 'PASSWORD_RESET'); 407 | } 408 | 409 | private function passwordResetResponse($oob_link, $param) { 410 | return array( 411 | 'email' => $param['email'], 412 | 'oobLink' => $oob_link, 413 | 'action' => 'RESET_PASSWORD', 414 | 'response_body' => json_encode(array('success' => true))); 415 | } 416 | 417 | private function changeEmailRequest($param, $user_ip, $gitkit_token) { 418 | return array( 419 | 'email' => $param['oldEmail'], 420 | 'newEmail' => $param['newEmail'], 421 | 'userIp' => $user_ip, 422 | 'idToken' => $gitkit_token, 423 | 'requestType' => 'NEW_EMAIL_ACCEPT'); 424 | } 425 | 426 | private function emailChangeResponse($oob_link, $param) { 427 | return array( 428 | 'oldEmail' => $param['oldEmail'], 429 | 'newEmail' => $param['newEmail'], 430 | 'oobLink' => $oob_link, 431 | 'action' => 'CHANGE_EMAIL', 432 | 'response_body' => json_encode(array('success' => true))); 433 | } 434 | 435 | private function failureOobMsg($string) { 436 | return json_encode(array('response_body' => array('error' => $string))); 437 | } 438 | } 439 | -------------------------------------------------------------------------------- /src/GitkitClientException.php: -------------------------------------------------------------------------------- 1 | p12Key = $p12Key; 43 | $this->serviceAccountEmail = $serviceAccountEmail; 44 | $this->gitkitApisUrl = $gitkitApiUrl; 45 | $this->apiKey = $serverApiKey; 46 | $this->oauth2Client = $oauth2Client; 47 | $credentials = new Google_Auth_AssertionCredentials( 48 | $this->serviceAccountEmail, 49 | self::$GITKIT_SCOPE, 50 | $this->p12Key 51 | ); 52 | $this->oauth2Client->setAssertionCredentials($credentials); 53 | } 54 | 55 | /** 56 | * Downloads Gitkit public certs. 57 | * 58 | * @return array|string certs 59 | */ 60 | public function getGitkitCerts() { 61 | $certUrl = $this->gitkitApisUrl . 'publicKeys'; 62 | if ($this->apiKey) { 63 | // try server-key first 64 | return $this->oauth2Client->retrieveCertsFromLocation( 65 | $certUrl . '?key=' . $this->apiKey); 66 | } else { 67 | // fallback to service account 68 | $httpRequest = new Google_Http_Request($certUrl); 69 | $response = $this->oauth2Client->authenticatedRequest($httpRequest) 70 | ->getResponseBody(); 71 | return json_decode($response); 72 | } 73 | } 74 | 75 | /** 76 | * Invokes the GetAccountInfo API with email. 77 | * 78 | * @param string $email user email 79 | * @return array user account info 80 | */ 81 | public function getAccountInfoByEmail($email) { 82 | $data = array('email' => array ($email)); 83 | $result = $this->invokeGitkitApiWithServiceAccount('getAccountInfo', $data); 84 | return $result['users'][0]; 85 | } 86 | 87 | /** 88 | * Invokes the GetAccountInfo API with user id. 89 | * 90 | * @param string $userId user identifier 91 | * @return array user account info 92 | */ 93 | public function getAccountInfoById($userId) { 94 | $data = array('localId' => array ($userId)); 95 | $result = $this->invokeGitkitApiWithServiceAccount('getAccountInfo', $data); 96 | return $result['users'][0]; 97 | } 98 | 99 | /** 100 | * Invokes the SetAccountInfo API. 101 | * 102 | * @param Gitkit_Account $gitkitAccount account info to be updated 103 | * @return array updated account info 104 | */ 105 | public function updateAccount($gitkitAccount) { 106 | $data = array( 107 | 'email' => $gitkitAccount->getEmail(), 108 | 'localId' => $gitkitAccount->getUserId(), 109 | 'displayName' => $gitkitAccount->getDisplayName(), 110 | 'emailVerified' => $gitkitAccount->isEmailVerified(), 111 | 'photoUrl' => $gitkitAccount->getPhotoUrl() 112 | ); 113 | return $this->invokeGitkitApiWithServiceAccount('setAccountInfo', $data); 114 | } 115 | 116 | /** 117 | * Invokes the DeleteAccount API. 118 | * 119 | * @param string $userId user id 120 | * @return array server response 121 | */ 122 | public function deleteAccount($userId) { 123 | $data = array('localId' => $userId); 124 | return $this->invokeGitkitApiWithServiceAccount('deleteAccount', $data); 125 | } 126 | 127 | /** 128 | * Invokes the UploadAccount API. 129 | * 130 | * @param string $hashAlgorithm password hash algorithm. See Gitkit doc for 131 | * supported names. 132 | * @param string $hashKey raw key for the algorithm 133 | * @param array $accounts array of account info to be uploaded 134 | * @param null|int $rounds Rounds of the hash function 135 | * @param null|int $memoryCost Memory cost of the hash function 136 | */ 137 | public function uploadAccount($hashAlgorithm, $hashKey, $accounts, 138 | $rounds, $memoryCost) { 139 | $data = array( 140 | 'hashAlgorithm' => $hashAlgorithm, 141 | 'signerKey' => Google_Utils::urlSafeB64Encode($hashKey), 142 | 'users' => $accounts 143 | ); 144 | if ($rounds) { 145 | $data['rounds'] = $rounds; 146 | } 147 | if ($memoryCost) { 148 | $data['memoryCost'] = $memoryCost; 149 | } 150 | $this->invokeGitkitApiWithServiceAccount('uploadAccount', $data); 151 | } 152 | 153 | /** 154 | * Invokes the DownloadAccount API. 155 | * 156 | * @param string|null $nextPageToken next page token to download the next 157 | * pagination. 158 | * @param int $maxResults max results per request 159 | * @return array of accounts info and nextPageToken 160 | */ 161 | public function downloadAccount($nextPageToken = null, $maxResults = 10) { 162 | $data = array(); 163 | if ($nextPageToken) { 164 | $data['nextPageToken'] = $nextPageToken; 165 | } 166 | $data['maxResults'] = $maxResults; 167 | return $this->invokeGitkitApiWithServiceAccount('downloadAccount', $data); 168 | } 169 | 170 | /** 171 | * Invokes the GetOobConfirmationCode API. 172 | * 173 | * @param array $param parameters for the request 174 | * @return string the out-of-band code 175 | * @throws Gitkit_ClientException 176 | */ 177 | public function getOobCode($param) { 178 | $response = $this->invokeGitkitApiWithServiceAccount( 179 | 'getOobConfirmationCode', $param); 180 | if (isset($response['oobCode'])) { 181 | return $response['oobCode']; 182 | } else { 183 | throw new Gitkit_ClientException("can not get oob-code"); 184 | } 185 | } 186 | 187 | /** 188 | * Sends the authenticated request to Gitkit API. The request contains an 189 | * OAuth2 access_token generated from service account. 190 | * 191 | * @param string $method the API method name 192 | * @param array $data http post data for the api 193 | * @return array server response 194 | * @throws Gitkit_ClientException if input is invalid 195 | * @throws Gitkit_ServerException if there is server error 196 | */ 197 | public function invokeGitkitApiWithServiceAccount($method, $data) { 198 | $httpRequest = new Google_Http_Request( 199 | $this->gitkitApisUrl . $method, 200 | 'POST', 201 | null, 202 | json_encode($data)); 203 | $contentTypeHeader = array(); 204 | $contentTypeHeader['content-type'] = 'application/json; charset=UTF-8'; 205 | $httpRequest->setRequestHeaders($contentTypeHeader); 206 | $response = $this->oauth2Client->authenticatedRequest($httpRequest) 207 | ->getResponseBody(); 208 | return $this->checkGitkitError(json_decode($response, true)); 209 | } 210 | 211 | /** 212 | * Checks the error in the response. 213 | * 214 | * @param array $response server response to be checked 215 | * @return array the response if there is no error 216 | * @throws Gitkit_ClientException if input is invalid 217 | * @throws Gitkit_ServerException if there is server error 218 | */ 219 | public function checkGitkitError($response) { 220 | if (isset($response['error'])) { 221 | $error = $response['error']; 222 | if (!isset($error['code'])) { 223 | throw new Gitkit_ServerException('null error code from Gitkit server'); 224 | } else { 225 | $code = $error['code']; 226 | if (strpos($code, '4') === 0) { 227 | throw new Gitkit_ClientException($error['message']); 228 | } else { 229 | throw new Gitkit_ServerException($error['message']); 230 | } 231 | } 232 | } else { 233 | return $response; 234 | } 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /tests/AllTests.php: -------------------------------------------------------------------------------- 1 | setName('All Google Identity Toolkit PHP Client tests'); 31 | $suite->addTestSuite('GitkitClientTest'); 32 | $suite->addTestSuite('RpcHelperTest'); 33 | return $suite; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/GitkitClientTest.php: -------------------------------------------------------------------------------- 1 | config = array( 27 | 'clientId' => '924226504183.apps.googleusercontent.com', 28 | 'widgetUrl' => 'http://example.com/widget' 29 | ); 30 | $this->rpcStubBuilder = $this->getMockBuilder('Gitkit_RpcHelper') 31 | ->disableOriginalConstructor(); 32 | } 33 | 34 | public function testVerifyToken() { 35 | $rpcStub = $this->rpcStubBuilder->setMethods(array('getGitkitCerts')) 36 | ->getMock(); 37 | 38 | $rpcStub->expects($this->once()) 39 | ->method('getGitkitCerts') 40 | ->will($this->returnValue(TestData::getCerts())); 41 | 42 | $gitkitClient = Gitkit_Client::createFromConfig($this->config, $rpcStub); 43 | $user = $gitkitClient->validateToken(TestData::getToken()); 44 | $this->assertNotNull($user); 45 | } 46 | 47 | public function testGetAllUsers() { 48 | $rpcStub = $this->rpcStubBuilder->setMethods(array('downloadAccount')) 49 | ->getMock(); 50 | 51 | $page1 = array( 52 | 'nextPageToken' => 'page2_token', 53 | 'users' => array($this->userRecord(1), $this->userRecord(2))); 54 | $page2 = array( 55 | 'users' => array($this->userRecord(3))); 56 | 57 | $rpcStub->expects($this->any()) 58 | ->method('downloadAccount') 59 | ->withConsecutive( 60 | array($this->equalTo(null), $this->equalTo(10)), 61 | array($this->equalTo('page2_token'), $this->equalTo(10))) 62 | ->will($this->onConsecutiveCalls($page1, $page2)); 63 | 64 | $gitkitClient = Gitkit_Client::createFromConfig($this->config, $rpcStub); 65 | $iterator = $gitkitClient->getAllUsers(10); 66 | $index = 0; 67 | while ($iterator->valid()) { 68 | $index ++; 69 | $user = $iterator->current(); 70 | $expectedUser = $this->userRecord($index); 71 | $this->assertEquals($expectedUser['localId'], $user->getUserId()); 72 | $iterator->next(); 73 | } 74 | // 3 accounts are downloaded in total 75 | $this->assertEquals(3, $index); 76 | } 77 | 78 | public function testGetUserByEmail() { 79 | $testUser = $this->userRecord(0); 80 | $rpcStub = $this->rpcStubBuilder->setMethods(array('getAccountInfoByEmail')) 81 | ->getMock(); 82 | $rpcStub->expects($this->any()) 83 | ->method('getAccountInfoByEmail') 84 | ->with($testUser['email']) 85 | ->will($this->returnValue($testUser)); 86 | $gitkitClient = Gitkit_Client::createFromConfig($this->config, $rpcStub); 87 | $this->assertEquals( 88 | $testUser['localId'], 89 | $gitkitClient->getUserByEmail($testUser['email'])->getUserId()); 90 | } 91 | 92 | public function testOobForResetPassword() { 93 | $rpcStub = $this->rpcStubBuilder->setMethods(array('getOobCode')) 94 | ->getMock(); 95 | $rpcStub->expects($this->any()) 96 | ->method('getOobCode') 97 | ->will($this->returnValue('oob-code')); 98 | $gitkitClient = Gitkit_Client::createFromConfig($this->config, $rpcStub); 99 | $oobReq = array( 100 | 'action' => 'resetPassword', 101 | 'email' => 'user@example.com', 102 | 'response' => '100'); 103 | 104 | $oobResult = $gitkitClient->getOobResults($oobReq, '1.1.1.1'); 105 | 106 | $this->assertEquals($oobReq['email'], $oobResult['email']); 107 | $this->assertEquals('RESET_PASSWORD', $oobResult['action']); 108 | $this->assertEquals( 109 | 'http://example.com/widget?mode=resetPassword&oobCode=oob-code', 110 | $oobResult['oobLink']); 111 | } 112 | 113 | public function testGetEmailVerificationLink() { 114 | $rpcStub = $this->rpcStubBuilder->setMethods(array('getOobCode')) 115 | ->getMock(); 116 | $rpcStub->expects($this->any()) 117 | ->method('getOobCode') 118 | ->will($this->returnValue('oob-code')); 119 | $gitkitClient = Gitkit_Client::createFromConfig($this->config, $rpcStub); 120 | 121 | $verifyLink = $gitkitClient->getEmailVerificationLink('user@example.com'); 122 | 123 | $this->assertEquals( 124 | 'http://example.com/widget?mode=verifyEmail&oobCode=oob-code', 125 | $verifyLink); 126 | } 127 | 128 | public function testOobWithException() { 129 | $rpcStub = $this->rpcStubBuilder->setMethods(array('getOobCode')) 130 | ->getMock(); 131 | $rpcStub->expects($this->any()) 132 | ->method('getOobCode') 133 | ->will($this->throwException(new Gitkit_ClientException('error-msg'))); 134 | $gitkitClient = Gitkit_Client::createFromConfig($this->config, $rpcStub); 135 | $oobReq = array( 136 | 'action' => 'resetPassword', 137 | 'email' => 'user@example.com', 138 | 'response' => '100'); 139 | 140 | try { 141 | $gitkitClient->getOobResults('http://example.com/oob', 142 | $oobReq, '1.1.1.1'); 143 | } catch (Gitkit_ClientException $e) { 144 | $this->assertEquals('error-msg', $e->getMessage()); 145 | } 146 | } 147 | 148 | private function userRecord($index) { 149 | return array('email' => 'email-' . $index, 'localId' => 'user-' . $index); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /tests/RpcHelperTest.php: -------------------------------------------------------------------------------- 1 | rpcHelper = new Gitkit_RpcHelper( 25 | 'service-email', 26 | 'TestKey.p12', 27 | 'api-url', 28 | new Google_Auth_OAuth2(new Google_Client()), 29 | 'server-api-key'); 30 | } 31 | 32 | public function testClientError() { 33 | $errorMessage = 'error-msg'; 34 | $clientErrorResponse = array( 35 | 'error' => array( 36 | 'code' => 400, 37 | 'message' => $errorMessage)); 38 | try { 39 | $this->rpcHelper->checkGitkitError($clientErrorResponse); 40 | } catch (Gitkit_ClientException $e) { 41 | $this->assertEquals($errorMessage, $e->getMessage()); 42 | } 43 | } 44 | 45 | public function testServerError() { 46 | $errorMessage = 'error-msg'; 47 | $clientErrorResponse = array( 48 | 'error' => array( 49 | 'code' => 500, 50 | 'message' => $errorMessage)); 51 | try { 52 | $this->rpcHelper->checkGitkitError($clientErrorResponse); 53 | } catch (Gitkit_ServerException $e) { 54 | $this->assertEquals($errorMessage, $e->getMessage()); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/TestData.php: -------------------------------------------------------------------------------- 1 | self::$PEM_CERT); 44 | } 45 | 46 | public static function getToken() { 47 | return 48 | 'eyJhbGciOiJSUzI1NiIsImtpZCI6IjQwUW9aZyJ9.eyJpc3MiOiJodHRwczovL2dpdG' . 49 | 'tpdC5nb29nbGUuY29tLyIsImF1ZCI6IjkyNDIyNjUwNDE4My5hcHBzLmdvb2dsZXVzZ' . 50 | 'XJjb250ZW50LmNvbSIsImlhdCI6MTM5OTAwMTI0MywiZXhwIjoxNDAwMjEwODQzLCJ1' . 51 | 'c2VyX2lkIjoiMTIzNCIsImVtYWlsIjoiMTIzNEBleGFtcGxlLmNvbSIsInZlcmlmaWV' . 52 | 'kIjpmYWxzZX0.Gqe7jSu5f61-JujzfCGrr-mp8ZDjUaZit432pKLL-zJ8tbaBkVEpHK' . 53 | 'SIiQA1GoA7ettx6T3w2ETze0ECIeOaUTUWkwZS7bft53Wty8eGr8erIHVdKp4roh5jT' . 54 | '2ksMZywwrQSKRYkgME1I75CQRhG9LPHl0JdI1amqUYBFGgnIIFZ0nGcJ-j5DNXteQT4' . 55 | 'Yt1FC-Gedub0LUoD51ZclPAb3zT-r5oA6d-uBIw6dgD5U8liHuZ1xXEqkZ12bhVYF6c' . 56 | 'RC8hlIpeuxjajrOUmtT9sMpJSUIAU8NFgFrE1SkxZ2ss6Q8zn-lKiu8EFBw03ZzF53f' . 57 | '9OrwwvDrJTUZuGyu2Ssg'; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/TestKey.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/identity-toolkit-php-client/8dd6995f5dd0635c8c1adaff976b700acf7a6225/tests/TestKey.p12 --------------------------------------------------------------------------------