├── VERSION ├── Makefile ├── lib ├── Exception │ ├── ApiException.php │ ├── AuthenticationException.php │ ├── ApiConnectionException.php │ ├── ForbiddenException.php │ ├── BadRequestException.php │ ├── RateLimitingException.php │ ├── DIDTokenException.php │ ├── MagicException.php │ └── RequestException.php ├── MagicResponse.php ├── Util │ ├── Time.php │ ├── Http.php │ ├── Eth.php │ └── DidToken.php ├── Resource │ ├── Wallet.php │ ├── User.php │ └── Token.php ├── Magic.php └── HttpClient.php ├── .editorconfig ├── phpunit.xml ├── tests ├── Util │ ├── HttpTest.php │ ├── TimeTest.php │ ├── DidTokenTest.php │ └── EthTest.php ├── Exception │ ├── DidTokenExceptionTest.php │ ├── MagicExceptionTest.php │ ├── ApiExceptionTest.php │ ├── RequestExceptionTest.php │ ├── ForbiddenExceptionTest.php │ ├── BadRequestExceptionTest.php │ ├── RateLimitExceptionTest.php │ ├── ApiConnectionExceptionTest.php │ └── AuthenticationExceptionTest.php ├── MagicResponseTest.php ├── MagicTest.php ├── Resource │ ├── UserTest.php │ └── TokenTest.php └── HttpClientTest.php ├── .github ├── PULL_REQUEST_TEMPLATE.md └── ISSUE_TEMPLATE │ ├── question.md │ ├── feature_request.md │ └── bug_report.md ├── .gitignore ├── composer.json ├── CHANGELOG.md ├── init.php ├── LICENSE.txt ├── .php-cs-fixer.dist.php ├── README.md └── CONTRIBUTING.md /VERSION: -------------------------------------------------------------------------------- 1 | 1.0.0 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: install test format 2 | 3 | install: 4 | composer install 5 | 6 | test: 7 | ./vendor/bin/phpunit tests/$(TEST_FILE) 8 | 9 | format: 10 | php-cs-fixer fix -v --using-cache=no . 11 | -------------------------------------------------------------------------------- /lib/Exception/ApiException.php: -------------------------------------------------------------------------------- 1 | content = $content; 14 | $this->status_code = $status_code; 15 | $this->data = $resp_data; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/Util/Time.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | tests 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /lib/Util/Http.php: -------------------------------------------------------------------------------- 1 | _message = $message; 16 | } 17 | 18 | public function getErrorMessage() 19 | { 20 | return $this->_message; 21 | } 22 | 23 | public function getRepr() 24 | { 25 | return static::class . '(message=' . $this->_message . ')'; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/Exception/DidTokenExceptionTest.php: -------------------------------------------------------------------------------- 1 | dIDTokenException = new MagicAdmin\Exception\DIDTokenException('Magic is amazing'); 16 | } 17 | 18 | public function testGetRepr() 19 | { 20 | static::assertSame('MagicAdmin\\Exception\\DIDTokenException(message=Magic is amazing)', $this->dIDTokenException->getRepr()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### 📦 Pull Request 2 | 3 | [Provide a general summary of the pull request here.] 4 | 5 | ### 🗜 Versioning 6 | 7 | (Check _one!_) 8 | 9 | - [ ] Patch: Bug Fix? 10 | - [ ] Minor: New Feature? 11 | - [ ] Major: Breaking Change? 12 | 13 | ### ✅ Fixed Issues 14 | 15 | - [List any fixed issues here like: Fixes #XXXX] 16 | 17 | ### 🚨 Test instructions 18 | 19 | [Describe any additional context required to test the PR/feature/bug fix.] 20 | 21 | ### ⚠️ Update `CHANGELOG.md` 22 | 23 | - [ ] I have updated the `Upcoming Changes` section of `CHANGELOG.md` with context related to this Pull Request. 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Use this template to request help or ask a question. 4 | title: "[WHAT'S YOUR QUESTION?]" 5 | labels: ❓Question 6 | --- 7 | 8 | ### ✅ Prerequisites 9 | 10 | - [ ] Did you perform a cursory search of open issues? Is this question already asked elsewhere? 11 | - [ ] Are you reporting to the correct repository (`magic-admin-php`)? 12 | 13 | ### ❓ Question 14 | 15 | [Ask your question here, please be as detailed as possible!] 16 | 17 | ### 🌎 Environment 18 | 19 | | Software | Version(s) | 20 | | ------------------- | ---------- | 21 | | `magic-admin-php` | | 22 | | `php` | | 23 | | Operating System | | 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Use this template to request a new feature. 4 | title: "[DESCRIPTIVE FEATURE NAME]" 5 | labels: ✨Feature Request 6 | --- 7 | 8 | ### ✅ Prerequisites 9 | 10 | - [ ] Did you perform a cursory search of open issues? Is this feature already requested elsewhere? 11 | - [ ] Are you reporting to the correct repository (`magic-admin-php`)? 12 | 13 | ### ✨ Feature Request 14 | 15 | [Description of the feature.] 16 | 17 | ## 🧩 Context 18 | 19 | [Explain any additional context or rationale for this feature. What are you trying to accomplish?] 20 | 21 | ## 💻 Examples 22 | 23 | [Do you have any example(s) for the requested feature? If so, describe/demonstrate your example(s) here.] 24 | -------------------------------------------------------------------------------- /tests/Exception/MagicExceptionTest.php: -------------------------------------------------------------------------------- 1 | magicException = new MagicAdmin\Exception\MagicException('Magic is amazing'); 16 | } 17 | 18 | public function testGetErrorMessage() 19 | { 20 | static::assertSame('Magic is amazing', $this->magicException->getErrorMessage()); 21 | } 22 | 23 | public function testGetRepr() 24 | { 25 | static::assertSame('MagicAdmin\\Exception\\MagicException(message=Magic is amazing)', $this->magicException->getRepr()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore build files 2 | build/* 3 | 4 | # Mac OS X dumps these all over the place. 5 | .DS_Store 6 | 7 | # Ignore the SimpleTest library if it is installed to /test/. 8 | /test/simpletest/ 9 | 10 | # Ignore the /vendor/ directory for people using composer 11 | /vendor/ 12 | 13 | # If the vendor directory isn't being commited the composer.lock file should also be ignored 14 | composer.lock 15 | 16 | # Ignore PHPUnit coverage file 17 | clover.xml 18 | .phpunit.result.cache 19 | 20 | # Ignore IDE's configuration files 21 | .idea 22 | 23 | # Ignore PHP CS Fixer local config and cache 24 | .php_cs 25 | .php_cs.cache 26 | 27 | # Ignore PHPStan local config 28 | .phpstan.neon 29 | 30 | # Ignore phpDocumentor's local config and artifacts 31 | .phpdoc/* 32 | phpdoc.xml 33 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "magiclabs/magic-admin-php", 3 | "description": "Magic Admin PHP Library", 4 | "keywords": [ 5 | "magic", 6 | "link", 7 | "admin", 8 | "authentication", 9 | "passwordless", 10 | "oauth2", 11 | "webauthen" 12 | ], 13 | "homepage": "https://magic.link", 14 | "license": "MIT", 15 | "authors": [ 16 | { 17 | "name": "Magic Labs Inc.", 18 | "email": "support@magic.link" 19 | } 20 | ], 21 | "require": { 22 | "php": ">=5.6.0", 23 | "ext-curl": "*", 24 | "ext-gmp": "*", 25 | "kornrunner/keccak": "^1.1", 26 | "simplito/elliptic-php": "^1.0" 27 | }, 28 | "require-dev": { 29 | "phpunit/phpunit": "^8.0" 30 | }, 31 | "autoload": { 32 | "psr-4": { 33 | "MagicAdmin\\": "lib/" 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/Resource/Wallet.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/Util/Eth.php: -------------------------------------------------------------------------------- 1 | \substr($signature, 2, 64), 16 | 's' => \substr($signature, 66, 64), 17 | ]; 18 | $recid = \ord(\hex2bin(\substr($signature, 130, 2))) - 27; 19 | 20 | if ($recid !== ($recid & 1)) { 21 | return false; 22 | } 23 | 24 | $ec = new EC('secp256k1'); 25 | $pubkey = $ec->recoverPubKey($hash, $sign, $recid); 26 | 27 | return '0x' . \substr(Keccak::hash(\substr(\hex2bin($pubkey->encode('hex')), 1), 256), 24); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Exception/ApiExceptionTest.php: -------------------------------------------------------------------------------- 1 | apiException = new MagicAdmin\Exception\ApiException( 16 | 'Magic is amazing', 17 | 'Magic is good', 18 | 503, 19 | ['magic' => 'link'], 20 | 'Magic is good', 21 | 'MAGIC_IS_GOOD', 22 | 'a=b&b=c', 23 | ['magic' => 'link'], 24 | 'post' 25 | ); 26 | } 27 | 28 | public function testGetRepr() 29 | { 30 | static::assertSame('MagicAdmin\\Exception\\ApiException(message=Magic is amazing, http_error_code=MAGIC_IS_GOOD, http_code=503)', $this->apiException->getRepr()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/Exception/RequestExceptionTest.php: -------------------------------------------------------------------------------- 1 | requestException = new MagicAdmin\Exception\RequestException( 16 | 'Magic is amazing', 17 | 'Magic is good', 18 | 500, 19 | ['magic' => 'link'], 20 | 'Magic is good', 21 | 'MAGIC_IS_GOOD', 22 | 'a=b&b=c', 23 | ['magic' => 'link'], 24 | 'post' 25 | ); 26 | } 27 | 28 | public function testGetRepr() 29 | { 30 | static::assertSame('MagicAdmin\\Exception\\RequestException(message=Magic is amazing, http_error_code=MAGIC_IS_GOOD, http_code=500)', $this->requestException->getRepr()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/Util/DidTokenTest.php: -------------------------------------------------------------------------------- 1 | public_address = '0x4B73C58370AEfcEf86A6021afCDe5673511376B2'; 17 | $this->issuer = 'did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2'; 18 | } 19 | 20 | public function testParsePublicAddressFromIssuer() 21 | { 22 | static::assertSame($this->public_address, \MagicAdmin\Util\DidToken::parse_public_address_from_issuer($this->issuer)); 23 | } 24 | 25 | public function testConstructIssuerWithPublicAddress() 26 | { 27 | static::assertSame('did:ethr:' . $this->public_address, \MagicAdmin\Util\DidToken::construct_issuer_with_public_address($this->public_address)); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Exception/ForbiddenExceptionTest.php: -------------------------------------------------------------------------------- 1 | forbiddenException = new MagicAdmin\Exception\ForbiddenException( 16 | 'Magic is amazing', 17 | 'Magic is good', 18 | 401, 19 | ['magic' => 'link'], 20 | 'Magic is good', 21 | 'MAGIC_IS_GOOD', 22 | 'a=b&b=c', 23 | ['magic' => 'link'], 24 | 'post' 25 | ); 26 | } 27 | 28 | public function testGetRepr() 29 | { 30 | static::assertSame('MagicAdmin\\Exception\\ForbiddenException(message=Magic is amazing, http_error_code=MAGIC_IS_GOOD, http_code=401)', $this->forbiddenException->getRepr()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/Exception/BadRequestExceptionTest.php: -------------------------------------------------------------------------------- 1 | badRequestException = new MagicAdmin\Exception\BadRequestException( 16 | 'Magic is amazing', 17 | 'Magic is good', 18 | 400, 19 | ['magic' => 'link'], 20 | 'Magic is good', 21 | 'MAGIC_IS_GOOD', 22 | 'a=b&b=c', 23 | ['magic' => 'link'], 24 | 'post' 25 | ); 26 | } 27 | 28 | public function testGetRepr() 29 | { 30 | static::assertSame('MagicAdmin\\Exception\\BadRequestException(message=Magic is amazing, http_error_code=MAGIC_IS_GOOD, http_code=400)', $this->badRequestException->getRepr()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/Exception/RateLimitExceptionTest.php: -------------------------------------------------------------------------------- 1 | rateLimitException = new MagicAdmin\Exception\RateLimitingException( 16 | 'Magic is amazing', 17 | 'Magic is good', 18 | 429, 19 | ['magic' => 'link'], 20 | 'Magic is good', 21 | 'MAGIC_IS_GOOD', 22 | 'a=b&b=c', 23 | ['magic' => 'link'], 24 | 'post' 25 | ); 26 | } 27 | 28 | public function testGetRepr() 29 | { 30 | static::assertSame('MagicAdmin\\Exception\\RateLimitingException(message=Magic is amazing, http_error_code=MAGIC_IS_GOOD, http_code=429)', $this->rateLimitException->getRepr()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/Exception/ApiConnectionExceptionTest.php: -------------------------------------------------------------------------------- 1 | apiConnectionException = new MagicAdmin\Exception\ApiConnectionException( 16 | 'Magic is amazing', 17 | 'Magic is good', 18 | 500, 19 | ['magic' => 'link'], 20 | 'Magic is good', 21 | 'MAGIC_IS_GOOD', 22 | 'a=b&b=c', 23 | ['magic' => 'link'], 24 | 'post' 25 | ); 26 | } 27 | 28 | public function testGetRepr() 29 | { 30 | static::assertSame('MagicAdmin\\Exception\\ApiConnectionException(message=Magic is amazing, http_error_code=MAGIC_IS_GOOD, http_code=500)', $this->apiConnectionException->getRepr()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/Exception/AuthenticationExceptionTest.php: -------------------------------------------------------------------------------- 1 | authenticationException = new MagicAdmin\Exception\AuthenticationException( 16 | 'Magic is amazing', 17 | 'Magic is good', 18 | 403, 19 | ['magic' => 'link'], 20 | 'Magic is good', 21 | 'MAGIC_IS_GOOD', 22 | 'a=b&b=c', 23 | ['magic' => 'link'], 24 | 'post' 25 | ); 26 | } 27 | 28 | public function testGetRepr() 29 | { 30 | static::assertSame('MagicAdmin\\Exception\\AuthenticationException(message=Magic is amazing, http_error_code=MAGIC_IS_GOOD, http_code=403)', $this->authenticationException->getRepr()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/MagicResponseTest.php: -------------------------------------------------------------------------------- 1 | magicResponse = new \MagicAdmin\MagicResponse($this->content, $this->resp_data, $this->status_code); 19 | } 20 | 21 | public function testRetrievesContent() 22 | { 23 | static::assertSame($this->magicResponse->content, $this->content); 24 | } 25 | 26 | public function testRetrievesRespData() 27 | { 28 | static::assertSame($this->magicResponse->data, $this->resp_data); 29 | } 30 | 31 | public function testRetrievesStatusCode() 32 | { 33 | static::assertSame($this->magicResponse->status_code, $this->status_code); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Upcoming Changes 2 | 3 | #### Fixed 4 | 5 | - ... 6 | 7 | #### Changed 8 | 9 | - ... 10 | 11 | #### Added 12 | 13 | - ... 14 | 15 | ## `1.0.0` - 7/5/2023 16 | 17 | #### Added 18 | 19 | - PR-#21: Add Magic Connect Admin SDK support for Token Resource. 20 | - [Security Enhancement]: Validate `aud` using Magic client ID. 21 | - Pull client ID from Magic servers if not provided in constructor. 22 | 23 | 24 | ## `0.3.0` - 2/17/2023 25 | 26 | #### Added 27 | 28 | - PR-#19: Add additional parameters to HttpClient user-agent 29 | 30 | ## `0.2.0` - 11/29/2022 31 | 32 | #### Added 33 | 34 | - PR-#18: Support mult-chain wallets in get_metadata calls 35 | 36 | ## `0.1.3` - 6/26/2022 37 | 38 | #### Fixed 39 | 40 | - PR-#14: Fix problems reported by PHPStan 41 | 42 | ## `0.1.2` - 12/22/2020 43 | 44 | #### Changed 45 | 46 | - PR-#8: Add support for runtimes without gmp 47 | 48 | ## `0.1.1` - 11/30/2020 49 | 50 | #### Changed 51 | 52 | - PR-#7: Use `isset` to check if a key exists in the claim array to support PHP7.4 53 | -------------------------------------------------------------------------------- /init.php: -------------------------------------------------------------------------------- 1 | getMessage() . ')' 28 | ); 29 | } 30 | } 31 | 32 | public static function construct_issuer_with_public_address($public_address) 33 | { 34 | return 'did:ethr:' . $public_address; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Use this template to report a bug. 4 | title: "[DESCRIPTIVE BUG NAME]" 5 | labels: 🐛 Bug Report, 🔍 Needs Triage 6 | --- 7 | 8 | ### ✅ Prerequisites 9 | 10 | - [ ] Did you perform a cursory search of open issues? Is this bug already reported elsewhere? 11 | - [ ] Are you running the latest SDK version? 12 | - [ ] Are you reporting to the correct repository (`magic-admin-php`)? 13 | 14 | ### 🐛 Description 15 | 16 | [Description of the bug.] 17 | 18 | ### 🧩 Steps to Reproduce 19 | 20 | 1. [First Step] 21 | 2. [Second Step] 22 | 3. [and so on...] 23 | 24 | ### 🤔 Expected behavior 25 | 26 | [What you expected to happen?] 27 | 28 | ### 😮 Actual behavior 29 | 30 | [What actually happened? Please include any error stack traces you encounter.] 31 | 32 | ### 💻 Code Sample 33 | 34 | [If possible, please provide a code repository, gist, code snippet or sample files to reproduce the issue.] 35 | 36 | ### 🌎 Environment 37 | 38 | | Software | Version(s) | 39 | | ------------------- | ---------- | 40 | | `magic-admin-php` | | 41 | | `php` | | 42 | | Operating System | | 43 | -------------------------------------------------------------------------------- /lib/Magic.php: -------------------------------------------------------------------------------- 1 | api_secret_key = $api_secret_key; 24 | $request_client = new \MagicAdmin\HttpClient($api_secret_key, $timeout, $retries, $backoff_factor); 25 | if ($client_id != null) { 26 | $this->client_id = $client_id; 27 | } else { 28 | $this->client_id = $this->_get_client_id($request_client); 29 | } 30 | $this->token = new \MagicAdmin\Resource\Token($this->client_id); 31 | $this->user = new \MagicAdmin\Resource\User( 32 | $request_client, 33 | $this->token 34 | ); 35 | } 36 | 37 | public function _set_platform($platform) 38 | { 39 | $this->user->_set_platform($platform); 40 | } 41 | 42 | public function _get_client_id($request_client) 43 | { 44 | $response = $request_client->request('get', '/v1/admin/client/get', []); 45 | 46 | return $response->data->client_id; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | setRiskyAllowed(true) 6 | ->setRules([ 7 | // Rulesets 8 | '@PSR2' => true, 9 | '@PhpCsFixer' => true, 10 | '@PhpCsFixer:risky' => true, 11 | '@PHP56Migration:risky' => true, 12 | '@PHPUnit57Migration:risky' => true, 13 | 14 | // Additional rules 15 | 'fopen_flags' => true, 16 | 'linebreak_after_opening_tag' => true, 17 | 'native_function_invocation' => true, 18 | 'ordered_imports' => true, 19 | 20 | // --- Diffs from @PhpCsFixer / @PhpCsFixer:risky --- 21 | 22 | // This is just prettier / easier to read. 23 | 'concat_space' => ['spacing' => 'one'], 24 | 25 | // This causes strange ordering with codegen'd classes. We might be 26 | // able to enable this if we update codegen to output class elements 27 | // in the correct order. 28 | 'ordered_class_elements' => false, 29 | 30 | // Keep this disabled to avoid unnecessary diffs in PHPDoc comments of 31 | // codegen'd classes. 32 | 'phpdoc_align' => false, 33 | 34 | // This is a "risky" rule that causes a bug in our codebase. 35 | // Specifically, in `StripeObject.updateAttributes` we construct new 36 | // `StripeObject`s for metadata. We can't use `self` there because it 37 | // needs to be a raw `StripeObject`. 38 | 'self_accessor' => false, 39 | ]) 40 | ; 41 | -------------------------------------------------------------------------------- /lib/Exception/RequestException.php: -------------------------------------------------------------------------------- 1 | http_status = $http_status; 33 | $this->http_code = $http_code; 34 | $this->http_resp_data = $http_resp_data; 35 | $this->http_message = $http_message; 36 | $this->http_error_code = $http_error_code; 37 | $this->http_request_params = $http_request_params; 38 | $this->http_request_data = $http_request_data; 39 | $this->http_method = $http_method; 40 | } 41 | 42 | public function getRepr() 43 | { 44 | return static::class . 45 | '(message=' . $this->_message . 46 | ', http_error_code=' . $this->http_error_code . 47 | ', http_code=' . $this->http_code . ')'; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/MagicTest.php: -------------------------------------------------------------------------------- 1 | api_secret_key = 'magic_admin'; 26 | $this->timeout = 10; 27 | $this->retries = 3; 28 | $this->backoff_factor = 0.02; 29 | $this->magic = new \MagicAdmin\Magic($this->api_secret_key, $this->timeout, $this->retries, $this->backoff_factor, $this->client_id); 30 | $this->mockedHttpClient = $this->createMock(HttpClient::class); 31 | } 32 | 33 | public function testRetrievesApiSecretKey() 34 | { 35 | static::assertSame($this->magic->api_secret_key, $this->api_secret_key); 36 | } 37 | 38 | public function testRetrievesTimeout() 39 | { 40 | static::assertSame($this->magic->user->request_client->_timeout, $this->timeout); 41 | } 42 | 43 | public function testRetrievesRetries() 44 | { 45 | static::assertSame($this->magic->user->request_client->_retries, $this->retries); 46 | } 47 | 48 | public function testRetrievesBackoffFactor() 49 | { 50 | static::assertSame($this->magic->user->request_client->_backoff_factor, $this->backoff_factor); 51 | } 52 | 53 | public function testRetrievesClientId() 54 | { 55 | static::assertSame($this->magic->client_id, $this->client_id); 56 | } 57 | 58 | public function testRetrievesClientIdFromMagic() 59 | { 60 | $clientId = 'test_client_id'; 61 | $this->mockedHttpClient->method('request') 62 | ->willReturn((object)[ 63 | 'data' => (object)[ 64 | 'client_id' => $clientId 65 | ] 66 | ]); 67 | static::assertSame($clientId, $this->magic->_get_client_id($this->mockedHttpClient)); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /lib/Resource/User.php: -------------------------------------------------------------------------------- 1 | request_client = $request_client; 16 | $this->token = $token; 17 | } 18 | 19 | public function get_metadata_by_issuer_and_wallet($issuer, $wallet_type) 20 | { 21 | return $this->request_client->request('get', $this->v1_user_info, ['issuer' => $issuer, 'wallet_type' => $wallet_type]); 22 | } 23 | 24 | public function get_metadata_by_issuer($issuer) 25 | { 26 | return $this->get_metadata_by_issuer_and_wallet($issuer, Wallet::NONE); 27 | } 28 | 29 | public function get_metadata_by_public_address_and_wallet($public_address, $wallet_type) 30 | { 31 | return $this->get_metadata_by_issuer( 32 | \MagicAdmin\Util\DidToken::construct_issuer_with_public_address($public_address), 33 | $wallet_type 34 | ); 35 | } 36 | 37 | public function get_metadata_by_public_address($public_address) 38 | { 39 | return $this->get_metadata_by_public_address_and_wallet($public_address, Wallet::NONE); 40 | } 41 | 42 | public function get_metadata_by_token_and_wallet($did_token, $wallet_type) 43 | { 44 | return $this->get_metadata_by_issuer($this->token->get_issuer($did_token), $wallet_type); 45 | } 46 | 47 | public function get_metadata_by_token($did_token) 48 | { 49 | return $this->get_metadata_by_token_and_wallet($did_token, Wallet::NONE); 50 | } 51 | 52 | public function logout_by_issuer($issuer) 53 | { 54 | return $this->request_client->request('post', $this->v2_user_logout, null, ['issuer' => $issuer]); 55 | } 56 | 57 | public function logout_by_public_address($public_address) 58 | { 59 | return $this->logout_by_issuer( 60 | \MagicAdmin\Util\DidToken::construct_issuer_with_public_address($public_address) 61 | ); 62 | } 63 | 64 | public function logout_by_token($did_token) 65 | { 66 | return $this->logout_by_issuer($this->token->get_issuer($did_token)); 67 | } 68 | 69 | public function _set_platform($platform) 70 | { 71 | $this->request_client->_set_platform($platform); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/Util/EthTest.php: -------------------------------------------------------------------------------- 1 | '0xbe93f9bacbcffc8ee6663f2647917ed7a20a57bb', 17 | 'message' => 'hello world', 18 | 'signature' => '0xce909e8ea6851bc36c007a0072d0524b07a3ff8d4e623aca4c71ca8e57250c4d0a3fc38fa8fbaaa81ead4b9f6bd03356b6f8bf18bccad167d78891636e1d69561b', 19 | ], 20 | [ 21 | 'address' => '0xe651c5051ce42241765bbb24655a791ff0ec8d13', 22 | 'message' => 'wee test message 18/09/2017 02:55PM', 23 | 'signature' => '0xf5ac62a395216a84bd595069f1bb79f1ee08a15f07bb9d9349b3b185e69b20c60061dbe5cdbe7b4ed8d8fea707972f03c21dda80d99efde3d96b42c91b2703211b', 24 | ], 25 | [ 26 | 'address' => '0x9283099a29556fcf8fff5b2cea2d4f67cb7a7a8b', 27 | 'message' => 'I am but a stack exchange post', 28 | 'signature' => '0x0cf7e2e1cbaf249175b8e004118a182eb378a0b78a7a741e72a0a34e970b59194aa4d9419352d181a4d1827abbad279ad4f5a7b60da5751b82fec4dde6f380a51b', 29 | ], 30 | [ 31 | 'address' => '0xb61f34dc82977e2b8c2bd747284b47ab94615bff', 32 | 'message' => 'I want to create a Account on this website. By I signing this text (using Ethereum personal_sign) I agree to the following conditions.', 33 | 'signature' => '0xbbdcdfb9fbe24d460a683633475c77a44072b527a127b159ffaaa043f5dc944105a1671c8b9df95e377d89ec17a1a0ed13f5caa33e5fa80bdf12391bf2e04e4f1c', 34 | ], 35 | [ 36 | 'address' => '0xb61f34dc82977e2b8c2bd747284b47ab94615bff', 37 | 'message' => 'I want to create a Account on this website. By I signing this text (using Ethereum personal_sign) I agree to the following conditions.', 38 | 'signature' => '0xbbdcdfb9fbe24d460a683633475c77a44072b527a127b159ffaaa043f5dc944105a1671c8b9df95e377d89ec17a1a0ed13f5caa33e5fa80bdf12391bf2e04e4f1c', 39 | ], 40 | ]; 41 | 42 | foreach ($tests as $test) { 43 | $actualSigner = Eth::ecRecover($test['message'], $test['signature']); 44 | $expectedSigner = $test['address']; 45 | 46 | static::assertSame($actualSigner, $expectedSigner); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/Resource/Token.php: -------------------------------------------------------------------------------- 1 | client_id = $client_id; 16 | } 17 | 18 | public $required_fields = [ 19 | 'iat', 20 | 'ext', 21 | 'nbf', 22 | 'iss', 23 | 'sub', 24 | 'aud', 25 | 'tid', 26 | ]; 27 | 28 | public function _check_required_fields($claim) 29 | { 30 | $missing_fields = []; 31 | foreach ($this->required_fields as $field) { 32 | if (\is_array($claim) && !isset($claim[$field])) { 33 | $missing_fields[] = $field; 34 | } 35 | } 36 | 37 | if (\count($missing_fields) > 0) { 38 | throw new \MagicAdmin\Exception\DIDTokenException( 39 | 'DID token is missing required field(s):' . json_encode($missing_fields) 40 | ); 41 | } 42 | 43 | return null; 44 | } 45 | 46 | public function decode($did_token) 47 | { 48 | try { 49 | $decoded_did_token = json_decode(utf8_decode(base64_decode($did_token, true))); 50 | } catch (\Exception $e) { 51 | throw new \MagicAdmin\Exception\DIDTokenException( 52 | 'DID token is malformed. It has to be a based64 encoded JSON serialized string. DIDTokenException(' . $e->getMessage() . ')' 53 | ); 54 | } 55 | 56 | if (EXPECTED_DID_TOKEN_CONTENT_LENGTH !== \count($decoded_did_token)) { 57 | throw new \MagicAdmin\Exception\DIDTokenException( 58 | 'DID token is malformed. It has to have two parts [proof, claim].' 59 | ); 60 | } 61 | 62 | $proof = $decoded_did_token[0]; 63 | 64 | try { 65 | $claim = json_decode($decoded_did_token[1]); 66 | } catch (\Exception $e) { 67 | throw new \MagicAdmin\Exception\DIDTokenException( 68 | 'DID token is malformed. Given claim should be a JSON serialized string. DIDTokenException(' . $e->getMessage() . ')' 69 | ); 70 | } 71 | 72 | $this->_check_required_fields($claim); 73 | 74 | return [$proof, $claim]; 75 | } 76 | 77 | public function get_issuer($did_token) 78 | { 79 | list($proof, $claim) = $this->decode($did_token); 80 | 81 | return $claim->iss; 82 | } 83 | 84 | public function get_public_address($did_token) 85 | { 86 | return \MagicAdmin\Util\DidToken::parse_public_address_from_issuer($this->get_issuer($did_token)); 87 | } 88 | 89 | public function validate($did_token) 90 | { 91 | list($proof, $claim) = $this->decode($did_token); 92 | 93 | $recovered_address = Eth::ecRecover(json_encode($claim), $proof); 94 | 95 | if ($recovered_address !== strtolower($this->get_public_address($did_token))) { 96 | throw new \MagicAdmin\Exception\DIDTokenException( 97 | 'Signature mismatch between "proof" and "claim". Please generate a new token with an intended issuer.' 98 | ); 99 | } 100 | 101 | $current_time_in_s = \MagicAdmin\Util\Time::epoch_time_now(); 102 | 103 | if ($current_time_in_s > $claim->ext) { 104 | throw new \MagicAdmin\Exception\DIDTokenException( 105 | 'Given DID token has expired. Please generate a new one.' 106 | ); 107 | } 108 | 109 | if ($current_time_in_s < \MagicAdmin\Util\Time::apply_did_token_nbf_grace_period($claim->nbf)) { 110 | throw new \MagicAdmin\Exception\DIDTokenException( 111 | 'Given DID token cannot be used at this time. Please check the "nbf" field and regenerate a new token with a suitable value.' 112 | ); 113 | } 114 | 115 | if ($claim->aud !== $this->client_id) { 116 | throw new \MagicAdmin\Exception\DIDTokenException( 117 | 'Audience does not match client ID. Please ensure your secret key matches the application which generated the DID token.' 118 | ); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Magic Admin PHP SDK 2 | 3 | The Magic Admin PHP SDK provides convenient ways for developers to interact with Magic API endpoints and an array of utilities to handle [DID Token](https://docs.magic.link/tutorials/decentralized-id). 4 | 5 | ## Table of Contents 6 | 7 | * [Documentation](#documentation) 8 | * [Installation](#installation) 9 | * [Quick Start](#quick-start) 10 | * [Changelog](#changelog) 11 | * [License](#license) 12 | 13 | ## Documentation 14 | See the [Magic doc](https://magic.link/docs/auth/api-reference/server-side-sdks/php)! 15 | 16 | ## Installation 17 | 18 | ### Composer 19 | 20 | You can install the bindings via [Composer](https://getcomposer.org/). Run the following command: 21 | 22 | ```bash 23 | composer require magiclabs/magic-admin-php 24 | ``` 25 | 26 | To use the bindings, use Composer's [autoload](https://getcomposer.org/doc/01-basic-usage.md#autoloading): 27 | 28 | ```php 29 | require_once __DIR__ . '/vendor/autoload.php'; 30 | ``` 31 | 32 | ### Manual Installation 33 | 34 | If you do not wish to use Composer, you can download the [latest release](https://github.com/magiclabs/magic-admin-php). Then, to use the bindings, include the `init.php` file. 35 | 36 | ```php 37 | require_once __DIR__ . '/path/to/magic-admin-php/init.php'; 38 | ``` 39 | 40 | ### Dependencies 41 | 42 | The bindings require the following extensions in order to work properly. If you use Composer, these dependencies should be handled automatically. If you install manually, you'll want to make sure that these extensions are available. 43 | 44 | - [`curl`](https://secure.php.net/manual/en/book.curl.php) 45 | - [`gmp`](https://www.php.net/manual/en/book.gmp.php) or [`bcmath`](https://www.php.net/manual/en/book.bc.php) see below 46 | 47 | For optimal performance ensure that your platform has the `gmp` extension installed. If your platform does not support `gmp` then `bcmath` may be used as an alternative, but note that `bcmath` is significantly slower than `gmp`. 48 | 49 | Since `gmp` is a required dependency you may need to use the `--ignore-platform-reqs` flag when runnining `composer install` on a platform without the `gmp` extension. 50 | 51 | ### Prerequisites 52 | 53 | PHP 5.6.0 and later. 54 | 55 | ## Quick Start 56 | 57 | Simple usage for login: 58 | 59 | ```php 60 | require_once __DIR__ . '/vendor/autoload.php'; 61 | 62 | $did_token = \MagicAdmin\Util\Http::parse_authorization_header_value( 63 | $authorization_header 64 | ); 65 | 66 | if ($did_token === null) { 67 | // DIDT is missing from the original HTTP request header. You can handle this by 68 | // remapping it to your application error. 69 | } 70 | 71 | $magic = new \MagicAdmin\Magic(''); 72 | 73 | try { 74 | $magic->token->validate($did_token); 75 | $issuer = $magic->token->get_issuer($did_token); 76 | } catch (\MagicAdmin\Exception\DIDTokenException $e) { 77 | // DIDT is malformed. You can handle this by remapping it 78 | // to your application error. 79 | } 80 | ``` 81 | 82 | ### Configure Network Strategy 83 | 84 | The `Magic` object also takes in `retries`, `timeout` and `backoff` as optional arguments at the object instantiation time so you can override those values for your application setup. 85 | 86 | ```php 87 | $magic = new \MagicAdmin\Magic( 88 | '', 89 | 5, // timeout 90 | 3, // retries 91 | 0.01 // backoff 92 | ); 93 | ``` 94 | 95 | See more examples from [Magic PHP doc](https://docs.magic.link/admin-sdk/php/examples/user-signup). 96 | 97 | ## Development 98 | 99 | Get [Composer](https://getcomposer.org/). For example, on Mac OS: 100 | 101 | ```bash 102 | brew install composer 103 | ``` 104 | 105 | Install dependencies: 106 | 107 | ```bash 108 | composer install 109 | ``` 110 | 111 | Install dependencies as mentioned above (which will resolve [PHPUnit](http://packagist.org/packages/phpunit/phpunit)), then you can run the test suite: 112 | 113 | ```bash 114 | ./vendor/bin/phpunit tests/ 115 | ``` 116 | 117 | Or to run an individual test file: 118 | 119 | ```bash 120 | ./vendor/bin/phpunit tests/MagicTest.php 121 | ``` 122 | 123 | The library uses [PHP CS Fixer](https://github.com/FriendsOfPHP/PHP-CS-Fixer) for code formatting. 124 | PHP CS Fixer must be [installed globally](https://cs.symfony.com/doc/installation.html). 125 | Code must be formatted before PRs are submitted. Run the formatter with: 126 | 127 | ```bash 128 | php-cs-fixer fix -v --using-cache=no . 129 | ``` 130 | 131 | ## Changelog 132 | 133 | See [Changelog](./CHANGELOG.md) 134 | 135 | ## License 136 | 137 | See [License](./LICENSE.txt) 138 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via an **issue**. This can be a feature request or a bug report. After a maintainer has triaged your issue, you are welcome to collaborate on a pull request. If your change is small or uncomplicated, you are welcome to open an issue and pull request simultaneously. 4 | 5 | Please note we have a **code of conduct**, please follow it in all your interactions with the project. 6 | 7 | ## Setting up for Local Development 8 | 9 | 1. Fork this repostiory. 10 | 2. Clone your fork. 11 | 3. Create a new branch in your local repository with the following pattern: 12 | 13 | - For bug fixes: `bug/#[issue_number]/[descriptive_bug_name]` 14 | - For features: `feature/#[issue_number]/[descriptive_feature_name]` 15 | - For chores/the rest: `chore/[descriptive_chore_name]` 16 | 17 | 4. Install dependencies: `composer install` 18 | 5. Start building for development 19 | 20 | ## Opening a Pull Request 21 | 22 | 1. Update the **`Upcoming Changes`** section of [`CHANGELOG.md`](./CHANGELOG.md) with your fixes, changes, or additions. A maintainer will label your changes with a version number and release date once they are published. 23 | 2. Open a pull request from your fork/branch to the upstream `master` branch of _this_ repository. 24 | 3. A maintainer will review your code changes and offer feedback or suggestions if necessary. Once your changes are approved, a maintainer will merge the pull request for you and publish a release. 25 | 26 | ## Contributor Covenant Code of Conduct 27 | 28 | ### Our Pledge 29 | 30 | We as members, contributors, and leaders pledge to make participation in our 31 | community a harassment-free experience for everyone, regardless of age, body 32 | size, visible or invisible disability, ethnicity, sex characteristics, gender 33 | identity and expression, level of experience, education, socio-economic status, 34 | nationality, personal appearance, race, religion, or sexual identity 35 | and orientation. 36 | 37 | We pledge to act and interact in ways that contribute to an open, welcoming, 38 | diverse, inclusive, and healthy community. 39 | 40 | ### Our Standards 41 | 42 | Examples of behavior that contributes to a positive environment for our 43 | community include: 44 | 45 | - Demonstrating empathy and kindness toward other people 46 | - Being respectful of differing opinions, viewpoints, and experiences 47 | - Giving and gracefully accepting constructive feedback 48 | - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 49 | - Focusing on what is best not just for us as individuals, but for the overall community 50 | 51 | Examples of unacceptable behavior include: 52 | 53 | - The use of sexualized language or imagery, and sexual attention or advances of any kind 54 | - Trolling, insulting or derogatory comments, and personal or political attacks 55 | - Public or private harassment 56 | - Publishing others' private information, such as a physical or email address, without their explicit permission 57 | - Other conduct which could reasonably be considered inappropriate in a professional setting 58 | 59 | ### Enforcement Responsibilities 60 | 61 | Community leaders are responsible for clarifying and enforcing our standards of 62 | acceptable behavior and will take appropriate and fair corrective action in 63 | response to any behavior that they deem inappropriate, threatening, offensive, 64 | or harmful. 65 | 66 | Community leaders have the right and responsibility to remove, edit, or reject 67 | comments, commits, code, wiki edits, issues, and other contributions that are 68 | not aligned to this Code of Conduct, and will communicate reasons for moderation 69 | decisions when appropriate. 70 | 71 | ### Scope 72 | 73 | This Code of Conduct applies within all community spaces, and also applies when 74 | an individual is officially representing the community in public spaces. 75 | Examples of representing our community include using an official e-mail address, 76 | posting via an official social media account, or acting as an appointed 77 | representative at an online or offline event. 78 | 79 | ### Enforcement 80 | 81 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 82 | reported to the community leaders responsible for enforcement at [support@magic.link](mailto:support@magic.link). 83 | All complaints will be reviewed and investigated promptly and fairly. 84 | 85 | All community leaders are obligated to respect the privacy and security of the 86 | reporter of any incident. 87 | 88 | ### Enforcement Guidelines 89 | 90 | Community leaders will follow these Community Impact Guidelines in determining 91 | the consequences for any action they deem in violation of this Code of Conduct: 92 | 93 | #### 1. Correction 94 | 95 | **Community Impact**: Use of inappropriate language or other behavior deemed 96 | unprofessional or unwelcome in the community. 97 | 98 | **Consequence**: A private, written warning from community leaders, providing 99 | clarity around the nature of the violation and an explanation of why the 100 | behavior was inappropriate. A public apology may be requested. 101 | 102 | #### 2. Warning 103 | 104 | **Community Impact**: A violation through a single incident or series 105 | of actions. 106 | 107 | **Consequence**: A warning with consequences for continued behavior. No 108 | interaction with the people involved, including unsolicited interaction with 109 | those enforcing the Code of Conduct, for a specified period of time. This 110 | includes avoiding interactions in community spaces as well as external channels 111 | like social media. Violating these terms may lead to a temporary or 112 | permanent ban. 113 | 114 | #### 3. Temporary Ban 115 | 116 | **Community Impact**: A serious violation of community standards, including 117 | sustained inappropriate behavior. 118 | 119 | **Consequence**: A temporary ban from any sort of interaction or public 120 | communication with the community for a specified period of time. No public or 121 | private interaction with the people involved, including unsolicited interaction 122 | with those enforcing the Code of Conduct, is allowed during this period. 123 | Violating these terms may lead to a permanent ban. 124 | 125 | #### 4. Permanent Ban 126 | 127 | **Community Impact**: Demonstrating a pattern of violation of community 128 | standards, including sustained inappropriate behavior, harassment of an 129 | individual, or aggression toward or disparagement of classes of individuals. 130 | 131 | **Consequence**: A permanent ban from any sort of public interaction within 132 | the community. 133 | 134 | ### Attribution 135 | 136 | This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), 137 | version 2.0, available at 138 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 139 | 140 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 141 | enforcement ladder](https://github.com/mozilla/diversity). 142 | 143 | For answers to common questions about this code of conduct, see the FAQ at 144 | https://www.contributor-covenant.org/faq. Translations are available at 145 | https://www.contributor-covenant.org/translations. 146 | -------------------------------------------------------------------------------- /tests/Resource/UserTest.php: -------------------------------------------------------------------------------- 1 | mockedHttpClient = $this->createMock(HttpClient::class); 23 | $client_id = "client_id"; 24 | $this->token = new \MagicAdmin\Resource\Token($client_id); 25 | $this->user = new \MagicAdmin\Resource\User( 26 | $this->mockedHttpClient, 27 | $this->token 28 | ); 29 | $this->issuer = 'did:ethr:0xabA53bd22b2673C6c42ffA11C251B45D8CcBe4a4'; 30 | $this->public_address = '0xabA53bd22b2673C6c42ffA11C251B45D8CcBe4a4'; 31 | $this->wallet_type = \MagicAdmin\Resource\Wallet::SOLANA; 32 | $this->wallet = (object) [ 33 | 'network'=> 'MAINNET', 34 | 'public_address' => $this->public_address, 35 | 'wallet_type' => $this->wallet_type 36 | ]; 37 | $this->wallets = array($this->wallet); 38 | $this->magic_response = new \MagicAdmin\MagicResponse( 39 | (object) [ 40 | 'data' => (object) [ 41 | 'email' => 'test@user.com', 42 | 'issuer' => $this->issuer, 43 | 'public_address' => $this->public_address, 44 | 'wallets' => $this->wallets, 45 | ], 46 | 'error_code' => '', 47 | 'message' => '', 48 | 'status' => 'ok', 49 | ], 50 | (object) [ 51 | 'email' => 'test@user.com', 52 | 'issuer' => $this->issuer, 53 | 'public_address' => $this->public_address, 54 | 'wallets' => $this->wallets, 55 | ], 56 | 200 57 | ); 58 | } 59 | 60 | public function testGetMetadataByIssuer() 61 | { 62 | $mock = $this->createMock(\MagicAdmin\Resource\User::class); 63 | $mock->method('get_metadata_by_issuer')->with($this->issuer)->willReturn( 64 | $this->magic_response 65 | ); 66 | 67 | $meta_data = $mock->get_metadata_by_issuer($this->issuer); 68 | 69 | static::assertSame($meta_data->data->issuer, $this->issuer); 70 | static::assertSame($meta_data->data->public_address, $this->public_address); 71 | } 72 | 73 | public function testGetMetadataByIssuerAndWallet() 74 | { 75 | $mock = $this->createMock(\MagicAdmin\Resource\User::class); 76 | $mock->method('get_metadata_by_issuer_and_wallet')->with($this->issuer, $this->wallet_type)->willReturn( 77 | $this->magic_response 78 | ); 79 | 80 | $meta_data = $mock->get_metadata_by_issuer_and_wallet($this->issuer, $this->wallet_type); 81 | 82 | static::assertSame($meta_data->data->issuer, $this->issuer); 83 | static::assertSame($meta_data->data->public_address, $this->public_address); 84 | static::assertSame($meta_data->data->wallets[0]->wallet_type, $this->wallet_type); 85 | } 86 | 87 | public function testGetMetadataByIssuerAndAnyWallet() 88 | { 89 | $mock = $this->createMock(\MagicAdmin\Resource\User::class); 90 | $mock->method('get_metadata_by_issuer_and_wallet')->with($this->issuer, \MagicAdmin\Resource\Wallet::ANY)->willReturn( 91 | $this->magic_response 92 | ); 93 | 94 | $meta_data = $mock->get_metadata_by_issuer_and_wallet($this->issuer, \MagicAdmin\Resource\Wallet::ANY); 95 | 96 | static::assertSame($meta_data->data->issuer, $this->issuer); 97 | static::assertSame($meta_data->data->public_address, $this->public_address); 98 | static::assertSame(count($meta_data->data->wallets), 1); 99 | } 100 | 101 | public function testGetMetadataByPublicAddressAndWallet() 102 | { 103 | $mock = $this->createMock(\MagicAdmin\Resource\User::class); 104 | $mock->method('get_metadata_by_public_address_and_wallet')->with($this->public_address, $this->wallet_type)->willReturn( 105 | $this->magic_response 106 | ); 107 | 108 | $meta_data = $mock->get_metadata_by_public_address_and_wallet($this->public_address, $this->wallet_type); 109 | 110 | static::assertSame($meta_data->data->issuer, $this->issuer); 111 | static::assertSame($meta_data->data->public_address, $this->public_address); 112 | static::assertSame($meta_data->data->wallets[0]->wallet_type, $this->wallet_type); 113 | } 114 | 115 | public function testGetMetadataByIssuerAndNoneWallet() 116 | { 117 | $mock = $this->createMock(\MagicAdmin\Resource\User::class); 118 | $mock->method('get_metadata_by_issuer_and_wallet')->with($this->issuer, \MagicAdmin\Resource\Wallet::NONE)->willReturn( 119 | $this->magic_response 120 | ); 121 | 122 | $meta_data = $mock->get_metadata_by_issuer_and_wallet($this->issuer, \MagicAdmin\Resource\Wallet::NONE); 123 | 124 | static::assertSame($meta_data->data->issuer, $this->issuer); 125 | static::assertSame($meta_data->data->public_address, $this->public_address); 126 | } 127 | 128 | public function testGetMetadataByPublicAddress() 129 | { 130 | $mock = $this->createMock(\MagicAdmin\Resource\User::class); 131 | $mock->method('get_metadata_by_public_address')->with($this->public_address)->willReturn( 132 | $this->magic_response 133 | ); 134 | 135 | $meta_data = $mock->get_metadata_by_public_address($this->public_address); 136 | 137 | static::assertSame($meta_data->data->issuer, $this->issuer); 138 | static::assertSame($meta_data->data->public_address, $this->public_address); 139 | } 140 | 141 | public function testGetMetadataByToken() 142 | { 143 | $did_token = 'magic_token'; 144 | $mock = $this->createMock(\MagicAdmin\Resource\User::class); 145 | $mock->method('get_metadata_by_token')->with($did_token)->willReturn( 146 | $this->magic_response 147 | ); 148 | 149 | $meta_data = $mock->get_metadata_by_token($did_token); 150 | 151 | static::assertSame($meta_data->data->issuer, $this->issuer); 152 | static::assertSame($meta_data->data->public_address, $this->public_address); 153 | } 154 | 155 | public function testGetMetadataByTokenAndWallet() 156 | { 157 | $did_token = 'magic_token'; 158 | $mock = $this->createMock(\MagicAdmin\Resource\User::class); 159 | $mock->method('get_metadata_by_token_and_wallet')->with($did_token, $this->wallet_type)->willReturn( 160 | $this->magic_response 161 | ); 162 | 163 | $meta_data = $mock->get_metadata_by_token_and_wallet($did_token, $this->wallet_type); 164 | 165 | static::assertSame($meta_data->data->issuer, $this->issuer); 166 | static::assertSame($meta_data->data->public_address, $this->public_address); 167 | static::assertSame($meta_data->data->wallets[0]->wallet_type, $this->wallet_type); 168 | } 169 | 170 | public function testLogoutByIssuer() 171 | { 172 | $mock = $this->createMock(\MagicAdmin\Resource\User::class); 173 | $mock->method('logout_by_issuer')->with($this->issuer)->willReturn( 174 | $this->magic_response 175 | ); 176 | 177 | $meta_data = $mock->logout_by_issuer($this->issuer); 178 | 179 | static::assertSame($meta_data->data->issuer, $this->issuer); 180 | static::assertSame($meta_data->data->public_address, $this->public_address); 181 | } 182 | 183 | public function testLogoutByPublicAddress() 184 | { 185 | $mock = $this->createMock(\MagicAdmin\Resource\User::class); 186 | $mock->method('logout_by_public_address')->with($this->public_address)->willReturn( 187 | $this->magic_response 188 | ); 189 | 190 | $meta_data = $mock->logout_by_public_address($this->public_address); 191 | 192 | static::assertSame($meta_data->data->issuer, $this->issuer); 193 | static::assertSame($meta_data->data->public_address, $this->public_address); 194 | } 195 | 196 | public function testLogoutByToken() 197 | { 198 | $did_token = 'magic_token'; 199 | $mock = $this->createMock(\MagicAdmin\Resource\User::class); 200 | $mock->method('logout_by_token') 201 | ->with($did_token) 202 | ->willReturn($this->magic_response) 203 | ; 204 | 205 | $meta_data = $mock->logout_by_token($did_token); 206 | 207 | static::assertSame($meta_data->data->issuer, $this->issuer); 208 | static::assertSame($meta_data->data->public_address, $this->public_address); 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /tests/Resource/TokenTest.php: -------------------------------------------------------------------------------- 1 | token = new \MagicAdmin\Resource\Token($client_id); 17 | } 18 | 19 | public function testCheckRequiredFields() 20 | { 21 | $claim = [ 22 | 'iat' => 1586764270, 23 | 'ext' => 11173528500, 24 | 'iss' => 'did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2', 25 | 'sub' => 'NjrA53ScQ8IV80NJnx4t3Shi9-kFfF5qavD2Vr0d1dc=', 26 | 'aud' => 'did:magic:731848cc-084e-41ff-bbdf-7f103817ea6b', 27 | 'nbf' => 1586764270, 28 | 'tid' => 'ebcc880a-ffc9-4375-84ae-154ccd5c746d', 29 | 'add' => '0x84d6839268a1af9111fdeccd396f303805dca2bc03450b7eb116e2f5fc8c5a722d1fb9af233aa73c5c170839ce5ad8141b9b4643380982da4bfbb0b11284988f1b', 30 | ]; 31 | static::assertSame($this->token->_check_required_fields($claim), null); 32 | } 33 | 34 | public function testCheckRequiredFieldsMissing() 35 | { 36 | $claim = [ 37 | 'iat' => 1586764270, 38 | 'ext' => 11173528500, 39 | 'iss' => 'did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2', 40 | 'sub' => 'NjrA53ScQ8IV80NJnx4t3Shi9-kFfF5qavD2Vr0d1dc=', 41 | 'aud' => 'did:magic:731848cc-084e-41ff-bbdf-7f103817ea6b', 42 | ]; 43 | 44 | try { 45 | $this->token->_check_required_fields($claim); 46 | } catch (\MagicAdmin\Exception\DIDTokenException $e) { 47 | static::assertSame($e->getErrorMessage(), 'DID token is missing required field(s):["nbf","tid"]'); 48 | } 49 | } 50 | } 51 | 52 | /** 53 | * @internal 54 | * @coversNothing 55 | */ 56 | final class TokenDecodeTest extends TestCase 57 | { 58 | public $token; 59 | 60 | protected function setUp(): void 61 | { 62 | $client_id = "client_id"; 63 | $this->token = new \MagicAdmin\Resource\Token($client_id); 64 | } 65 | 66 | public function testDecodeRaisesErrorIfDidTokenIsMalformed() 67 | { 68 | $did_token = 'magic_token'; // did token is malformed 69 | 70 | try { 71 | $this->token->decode($did_token); 72 | } catch (\MagicAdmin\Exception\DIDTokenException $e) { 73 | static::assertSame($e->message, 'DID token is malformed. It has to be a based64 encoded JSON serialized string. DIDTokenException().'); 74 | } 75 | } 76 | 77 | public function testDecodeRaisesErrorIfDidTokenIsMissingParts() 78 | { 79 | $did_token = 'WyJ7XCJpYXRcIjoxNjAwOTYxNDgyLFwiZXh0XCI6MTYwMDk2MjM4MixcImlzc1wiOlwiZGlkOmV0aHI6MHhhYkE1M2JkMjJiMjY3M0M2YzQyZmZBMTFDMjUxQjQ1RDhDY0JlNGE0XCIsXCJzdWJcIjpcIlFrQl82dFhQRWFxRjktLTFGU08yMTZGZnRDLW9EVFJadG5zNmxScWZiYjA9XCIsXCJhdWRcIjpcImRpZDptYWdpYzpjODEwZTZjYi1hMWNlLTQyZTgtOWU5NC1iOWExZjc5ZTIzMjZcIixcImFkZFwiOlwiMHhiMjQ4MWY5ZWNlNDY4YWExN2I1YTk0M2VmOTQwNjNiY2E0MDczMjYxZjBmYzE4NjEzNDk4MTg0OWIzNmIyOTk1N2M4ZTA0M2NhNGE2MzE3ZjdmM2IyOWQ0NGYxMDhmMTg3ZDBmOTM2YjFjMjE3YWEzNGZkMjA4MWQ2NTdkMzRmMDFjXCJ9Il0='; // proof is missing 80 | 81 | try { 82 | $this->token->decode($did_token); 83 | } catch (\MagicAdmin\Exception\DIDTokenException $e) { 84 | static::assertSame($e->message, 'DID token is malformed. It has to have two parts [proof, claim].'); 85 | } 86 | } 87 | 88 | public function testDecodeRaisesErrorIfClaimIsNotJsonSerializable() 89 | { 90 | $did_token = 'YXJyYXkgKAogIDAgPT4gJzB4MTIzMjRjNjFlYTFkMjQ1ZjVmNmFlYTc5ODQ5Y2NjMDM5ZjllMTU4MjcyOWQyODZiNmM4YTZkNjE0OWUyOTgwNDQyMzA3NDY4NWNmYThiOGFlMGJhMzMwOTI0NjMyMTg4Y2IyNmM3NjkwNmQ0MTY2OTg3ZDczZDgyOWI4NTJjNzgxYicsCiAgMSA9PiAneyJpYXQiOjE2MDA5NjE0ODIsImV4dCI6MTYwMDk2MjM4MiwiaXNzIjoiZGlkOmV0aHI6MHhhYkE1M2JkMjJiMjY3M0M2YzQyZmZBMTFDMjUxQjQ1RDhDY0JlNGE0Iiwic3ViIjoiUWtCXzZ0WFBFYXFGOS0tMUZTTzIxNkZmdEMtb0RUUlp0bnM2bFJxZmJiMD0iLCJhdWQiOiJkaWQ6bWFnaWM6YzgxMGU2Y2ItYTFjZS00MmU4LTllOTQtYjlhMWY3OWUyMzI2IiwibmJmIjoxNjAwOTYxNDgyLCJ0aWQiOiI0MzNjYmFlYy04YTlhLTQ5N2UtOTlkNy1mMjViYTdkNjBjMzEiLCJhZGQiOiIweGIyNDgxZjllY2U0NjhhYTE3YjVhOTQzZWY5NDA2M2JjYTQwNzMyNjFmMGZjMTg2MTM0OTgxODQ5YjM2YjI5OTU3YzhlMDQzY2E0YTYzMTdmN2YzYjI5ZDQ0ZjEwOGYxODdkMGY5MzZiMWMyMTdhYTM0ZmQyMDgxZDY1N2QzNGYwMWMifScsCik='; // Not json serialized 91 | 92 | try { 93 | $this->token->decode($did_token); 94 | } catch (\MagicAdmin\Exception\DIDTokenException $e) { 95 | static::assertSame($e->message, 'DID token is malformed. Given claim should be a JSON serialized string. DIDTokenException().'); 96 | } 97 | } 98 | } 99 | 100 | /** 101 | * @internal 102 | * @coversNothing 103 | */ 104 | final class TokenValidateTest extends TestCase 105 | { 106 | public $token; 107 | 108 | protected function setUp(): void 109 | { 110 | $client_id = 'did:magic:f54168e9-9ce9-47f2-81c8-7cb2a96b26ba'; 111 | $this->token = new \MagicAdmin\Resource\Token($client_id); 112 | } 113 | 114 | /** 115 | * @doesNotPerformAssertions 116 | */ 117 | public function testValidate() 118 | { 119 | $valid_did_token = 'WyIweGUwMjQzNTVlNDI5ZGNhZDM1MTdhZDk5ZWEzNDEwYWJmZDQ1YjBiNjM5OGIwNjY1NGRiYTQxNzljODdlMTYyNzgxNTc1YjA5ODFjNjU4ZjcwMjYwZTQ5MjMwZGE5NDg4YTA0ZDk5NzBlYjM4ZTZmZGRlY2Q2NTA5YTAyN2IwOGI5MWIiLCJ7XCJpYXRcIjoxNTg1MDExMjA0LFwiZXh0XCI6MTkwMDQxMTIwNCxcImlzc1wiOlwiZGlkOmV0aHI6MHhCMmVjOWI2MTY5OTc2MjQ5MWI2NTQyMjc4RTlkRkVDOTA1MGY4MDg5XCIsXCJzdWJcIjpcIjZ0RlhUZlJ4eWt3TUtPT2pTTWJkUHJFTXJwVWwzbTNqOERReWNGcU8ydHc9XCIsXCJhdWRcIjpcImRpZDptYWdpYzpmNTQxNjhlOS05Y2U5LTQ3ZjItODFjOC03Y2IyYTk2YjI2YmFcIixcIm5iZlwiOjE1ODUwMTEyMDQsXCJ0aWRcIjpcIjJkZGY1OTgzLTk4M2ItNDg3ZC1iNDY0LWJjNWUyODNhMDNjNVwiLFwiYWRkXCI6XCIweDkxZmJlNzRiZTZjNmJmZDhkZGRkZDkzMDExYjA1OWI5MjUzZjEwNzg1NjQ5NzM4YmEyMTdlNTFlMGUzZGYxMzgxZDIwZjUyMWEzNjQxZjIzZWI5OWNjYjM0ZTNiYzVkOTYzMzJmZGViYzhlZmE1MGNkYjQxNWU0NTUwMDk1MmNkMWNcIn0iXQ=='; 120 | 121 | $this->token->validate($valid_did_token); 122 | } 123 | 124 | public function testValidateRaisesErrorIfDidTokenHasInvalidSigner() 125 | { 126 | $invalid_signer_did_token = 'WyIweDBhNTk4NmE1NDdiMzNhMDAxODIxNmRiNjk0YzNiMDg3YTU3MTk1Nzg4ZTZmMDc2NDg4NzA2ZTQ3ZmFhNjFhYzMzZDczZTM4ZmM5ZDA0YzU2YWVmZWNiMTAxMDA4OGEwNmFlOWFiZTE5ZDIyYWQ4MzNiMDhhM2VlNWNmZWM5ZDQ0MWMiLCJ7XCJpYXRcIjoxNTg1MDEwODIxLFwiZXh0XCI6MTkwMDQxMDgyMSxcImlzc1wiOlwiXFxcImRpZDpldGhyOjB4MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMFxcXCJcIixcInN1YlwiOlwiNnRGWFRmUnh5a3dNS09PalNNYmRQckVNcnBVbDNtM2o4RFF5Y0ZxTzJ0dz1cIixcImF1ZFwiOlwiZGlkOm1hZ2ljOjMzZjAxNGVlLTNkZDUtNGRmZi1iYzE2LTgxNTU3MTFiN2UwMlwiLFwibmJmXCI6MTU4NTAxMDgyMSxcInRpZFwiOlwiOGEzYjdkZDUtZTFjZi00OTY1LWFlMmItZDIwZjE4OGU2ZWMyXCIsXCJhZGRcIjpcIjB4OTFmYmU3NGJlNmM2YmZkOGRkZGRkOTMwMTFiMDU5YjkyNTNmMTA3ODU2NDk3MzhiYTIxN2U1MWUwZTNkZjEzODFkMjBmNTIxYTM2NDFmMjNlYjk5Y2NiMzRlM2JjNWQ5NjMzMmZkZWJjOGVmYTUwY2RiNDE1ZTQ1NTAwOTUyY2QxY1wifSJd'; 127 | 128 | try { 129 | $this->token->validate($invalid_signer_did_token); 130 | } catch (\MagicAdmin\Exception\DIDTokenException $e) { 131 | static::assertSame($e->getMessage(), 'Signature mismatch between "proof" and "claim". Please generate a new token with an intended issuer.'); 132 | } 133 | } 134 | 135 | public function testValidateRaisesErrorIfDidTokenIsExpired() 136 | { 137 | $expired_did_token = 'WyIweGE3MDUzYzg3OTI2ZjMzZDBjMTZiMjMyYjYwMWYxZDc2NmRiNWY3YWM4MTg2MzUyMzY4ZjAyMzIyMGEwNzJjYzkzM2JjYjI2MmU4ODQyNWViZDA0MzcyZGU3YTc0NzMwYjRmYWYzOGU0ZjgwNmYzOTJjMTVkNzY2YmVkMjVlZmUxMWIiLCJ7XCJpYXRcIjoxNTg1MDEwODM1LFwiZXh0XCI6MTU4NTAxMDgzNixcImlzc1wiOlwiZGlkOmV0aHI6MHhCMmVjOWI2MTY5OTc2MjQ5MWI2NTQyMjc4RTlkRkVDOTA1MGY4MDg5XCIsXCJzdWJcIjpcIjZ0RlhUZlJ4eWt3TUtPT2pTTWJkUHJFTXJwVWwzbTNqOERReWNGcU8ydHc9XCIsXCJhdWRcIjpcImRpZDptYWdpYzpkNGMwMjgxYi04YzViLTQ5NDMtODUwOS0xNDIxNzUxYTNjNzdcIixcIm5iZlwiOjE1ODUwMTA4MzUsXCJ0aWRcIjpcImFjMmE4YzFjLWE4OWEtNDgwOC1hY2QxLWM1ODg1ZTI2YWZiY1wiLFwiYWRkXCI6XCIweDkxZmJlNzRiZTZjNmJmZDhkZGRkZDkzMDExYjA1OWI5MjUzZjEwNzg1NjQ5NzM4YmEyMTdlNTFlMGUzZGYxMzgxZDIwZjUyMWEzNjQxZjIzZWI5OWNjYjM0ZTNiYzVkOTYzMzJmZGViYzhlZmE1MGNkYjQxNWU0NTUwMDk1MmNkMWNcIn0iXQ=='; 138 | 139 | try { 140 | $this->token->validate($expired_did_token); 141 | } catch (\MagicAdmin\Exception\DIDTokenException $e) { 142 | static::assertSame($e->getMessage(), 'Given DID token has expired. Please generate a new one.'); 143 | } 144 | } 145 | 146 | public function testValidateRaisesErrorIfDidTokenCannotBeUsedYet() 147 | { 148 | $valid_future_marked_did_token = 'WyIweDkzZjRiNTViYzRlN2E1ZWJkZTdmMzVkYzczMWE5NWFmOGYwZjVlMWQyMWQ5ZDYwZWQxM2Y4YmYzMmNiN2UwOTQ1MDM0MGI1Y2IyNTIxODZkNWQ3OTFiOTAyODZhYmY1NzM3YzMxN2M5NzNhMmQzMGY0MWZmYmFlNGU0NTdmMjE4MWIiLCJ7XCJpYXRcIjoxNTkxOTE0NTgyLFwiZXh0XCI6MjIyMjcxNDU4MixcImlzc1wiOlwiZGlkOmV0aHI6MHg0YzMzMmQ5QzRhMmEwNjY1YzNmODg1MTU1YjlFOTFmZEIzMDBlRTc2XCIsXCJzdWJcIjpcIms4NUtaR09Ycl9vMTYxNGdFVGN6Yzlac0phTjV4cjF2TVFXSWhnbjQ1Slk9XCIsXCJhdWRcIjpcImRpZDptYWdpYzoyMWI4ZjRkZS02ZmIzLTQ0M2YtOGM0MC04ODcwODJjNDQ1MjNcIixcIm5iZlwiOjE5MDczMTQ1ODIsXCJ0aWRcIjpcIjVhMjhjMjQwLWRmYzYtNDg2Ni04ODk1LTVkYzBhOTVkNWJkN1wiLFwiYWRkXCI6XCIweGRlMmI1ODgyNjUyZGExOTY4YWNlZTIyYWUyNGI2OWYxNThlZjg1NDQzOGE0OTlmMThjZGZlZDU3MzEwOGIxNzExYjQ2OWQ3MzQ5NzdhNGQ4NGJlM2RiODc2OTBkZjFmZjk4MTVjN2Y3NDIxNjIxMGY4Y2JhMGJmYzQ2ZGIwYjhkMWNcIn0iXQ=='; 149 | 150 | try { 151 | $this->token->validate($valid_future_marked_did_token); 152 | } catch (\MagicAdmin\Exception\DIDTokenException $e) { 153 | static::assertSame($e->getMessage(), 'Given DID token cannot be used at this time. Please check the "nbf" field and regenerate a new token with a suitable value.'); 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /lib/HttpClient.php: -------------------------------------------------------------------------------- 1 | )` ' . 13 | 'object or use the environment variable, `MAGIC_API_SECRET_KEY`. You can ' . 14 | 'get your API secret key from https://dashboard.magic.link. If you are having ' . 15 | 'trouble, please don\'t hesitate to reach out to us at support@magic.link' 16 | ); 17 | 18 | class HttpClient 19 | { 20 | public $_timeout; 21 | public $_retries; 22 | public $_backoff_factor; 23 | public $_base_url; 24 | public $_api_secret_key; 25 | public $_platform = 'php'; 26 | public $ch; 27 | 28 | public function __construct($api_secret_key, $timeout, $retries, $backoff_factor) 29 | { 30 | $this->_api_secret_key = $api_secret_key; 31 | $this->_base_url = API_MAGIC_BASE_URL; 32 | $this->_timeout = $timeout; 33 | $this->_retries = $retries; 34 | $this->_backoff_factor = $backoff_factor; 35 | } 36 | 37 | public function _setup_curl() 38 | { 39 | $this->ch = \curl_init(); 40 | \curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, true); 41 | \curl_setopt($this->ch, CURLOPT_FORBID_REUSE, true); 42 | \curl_setopt($this->ch, CURLOPT_FOLLOWLOCATION, true); 43 | \curl_setopt($this->ch, CURLOPT_MAXREDIRS, 10); 44 | \curl_setopt($this->ch, CURLOPT_TIMEOUT, $this->_timeout); 45 | \curl_setopt($this->ch, CURLOPT_CONNECTTIMEOUT, 10); 46 | \curl_setopt($this->ch, CURLOPT_FAILONERROR, false); 47 | \curl_setopt($this->ch, CURLOPT_HTTPHEADER, $this->_get_request_headers()); 48 | } 49 | 50 | public function _get_request_headers() 51 | { 52 | if (null === $this->_api_secret_key) { 53 | throw new \MagicAdmin\Exception\AuthenticationException( 54 | API_SECRET_KEY_MISSING_MESSAGE 55 | ); 56 | } 57 | 58 | $headers = []; 59 | $headers[] = 'X-Magic-Secret-Key: ' . $this->_api_secret_key; 60 | $headers[] = 'User-Agent: ' . \json_encode($this->_get_user_agent()); 61 | 62 | return $headers; 63 | } 64 | 65 | public function _get_user_agent() 66 | { 67 | $user_agent = []; 68 | $user_agent[] = 'language: php'; 69 | $user_agent[] = 'sdk_version: ' . $this->get_version(); 70 | $user_agent[] = 'publisher: Magic Labs Inc.'; 71 | $user_agent[] = 'http_lib: magic-admin-php'; 72 | $user_agent[] = 'platform: '.$this->_platform; 73 | 74 | if (isset($_SERVER['SERVER_NAME'])) { 75 | $user_agent[] = 'server_name: '.$_SERVER['SERVER_NAME']; 76 | } 77 | 78 | return $user_agent; 79 | } 80 | 81 | public function request($method, $url, $params = null, $data = null) 82 | { 83 | list($content, $status_code) = $this->api_request($method, $url, $params, $data); 84 | 85 | return $this->_parse_and_convert_to_api_response( 86 | $content, 87 | $status_code, 88 | $method, 89 | $params, 90 | $data 91 | ); 92 | } 93 | 94 | public function api_request($method, $url, $params = null, $data = null) 95 | { 96 | try { 97 | if ('get' === $method) { 98 | $send_params = ''; 99 | if (\is_array($params)) { 100 | $send_params = \http_build_query($params, '', '&', PHP_QUERY_RFC3986); 101 | } else { 102 | throw new \MagicAdmin\Exception\BadRequestException( 103 | 'Query must be a string or array.' 104 | ); 105 | } 106 | 107 | $this->_setup_curl(); 108 | \curl_setopt($this->ch, CURLOPT_URL, $this->_base_url . $url . '?' . $send_params); 109 | 110 | $retries_number = 0; 111 | 112 | while (true) { 113 | $rcode = 0; 114 | $errno = 0; 115 | $info = null; 116 | 117 | $content = \curl_exec($this->ch); 118 | 119 | if (false === $content) { 120 | $errno = \curl_errno($this->ch); 121 | } else { 122 | $info = \curl_getinfo($this->ch); 123 | $rcode = $info['http_code']; 124 | } 125 | 126 | $should_retry = $this->check_retry($errno, $rcode, $retries_number); 127 | if ($should_retry) { 128 | ++$retries_number; 129 | \usleep((int) ($this->_backoff_factor * 1000000)); 130 | } else { 131 | break; 132 | } 133 | } 134 | 135 | \curl_close($this->ch); 136 | } elseif ('post' === $method) { 137 | $this->_setup_curl(); 138 | \curl_setopt($this->ch, CURLOPT_POST, true); 139 | \curl_setopt($this->ch, CURLOPT_POSTFIELDS, \json_encode($data)); 140 | \curl_setopt($this->ch, CURLOPT_URL, $this->_base_url . $url); 141 | $retries_number = 0; 142 | 143 | while (true) { 144 | $rcode = 0; 145 | $errno = 0; 146 | $info = null; 147 | 148 | $content = \curl_exec($this->ch); 149 | 150 | if (false === $content) { 151 | $errno = \curl_errno($this->ch); 152 | } else { 153 | $info = \curl_getinfo($this->ch); 154 | $rcode = $info['http_code']; 155 | } 156 | 157 | $should_retry = $this->check_retry($errno, $rcode, $retries_number); 158 | if ($should_retry) { 159 | ++$retries_number; 160 | \usleep((int) ($this->_backoff_factor * 1000000)); 161 | } else { 162 | break; 163 | } 164 | } 165 | \curl_close($this->ch); 166 | } 167 | } catch (\Exception $e) { 168 | throw new \MagicAdmin\Exception\ApiConnectionException( 169 | 'Unexpected error thrown while communicating to Magic. ' . 170 | 'Please reach out to support@magic.link if the problem continues. ' . 171 | 'Error message: ' . __CLASS__ . ' was raised - ' . $e->getMessage() 172 | ); 173 | } 174 | 175 | return [$content, $rcode]; 176 | } 177 | 178 | public function check_retry($errno, $rcode, $retries_number) 179 | { 180 | if ($retries_number >= $this->_retries) { 181 | return false; 182 | } 183 | 184 | // Retry on timeout-related problems (either on open or read). 185 | if (CURLE_OPERATION_TIMEOUTED === $errno || CURLE_COULDNT_CONNECT === $errno) { 186 | return true; 187 | } 188 | 189 | // 409 Conflict 190 | if (409 === $rcode) { 191 | return true; 192 | } 193 | 194 | // Retry on 500, 503, and other internal errors. 195 | if ($rcode >= 500) { 196 | return true; 197 | } 198 | 199 | return false; 200 | } 201 | 202 | public function _parse_and_convert_to_api_response($resp_content, $status_code, $method, $request_params, $request_data) 203 | { 204 | $resp_content = \json_decode($resp_content); 205 | 206 | if ($status_code >= 200 && $status_code < 300) { 207 | return new MagicResponse($resp_content, $resp_content->data, $status_code); 208 | } 209 | 210 | if (429 === $status_code) { 211 | throw new \MagicAdmin\Exception\RateLimitingException( 212 | '', 213 | $resp_content->status, 214 | $status_code, 215 | $resp_content->data, 216 | $resp_content->message, 217 | $resp_content->error_code, 218 | $request_params, 219 | $request_data, 220 | $method 221 | ); 222 | } 223 | if (400 === $status_code) { 224 | throw new \MagicAdmin\Exception\BadRequestException( 225 | '', 226 | $resp_content->status, 227 | $status_code, 228 | $resp_content->data, 229 | $resp_content->message, 230 | $resp_content->error_code, 231 | $request_params, 232 | $request_data, 233 | $method 234 | ); 235 | } 236 | if (401 === $status_code) { 237 | throw new \MagicAdmin\Exception\AuthenticationException( 238 | '', 239 | $resp_content->status, 240 | $status_code, 241 | $resp_content->data, 242 | $resp_content->message, 243 | $resp_content->error_code, 244 | $request_params, 245 | $request_data, 246 | $method 247 | ); 248 | } 249 | if (403 === $status_code) { 250 | throw new \MagicAdmin\Exception\ForbiddenException( 251 | '', 252 | $resp_content->status, 253 | $status_code, 254 | $resp_content->data, 255 | $resp_content->message, 256 | $resp_content->error_code, 257 | $request_params, 258 | $request_data, 259 | $method 260 | ); 261 | } 262 | 263 | throw new \MagicAdmin\Exception\ApiException( 264 | '', 265 | \property_exists($resp_content, 'status') ? $resp_content->status : null, 266 | $status_code, 267 | \property_exists($resp_content, 'data') ? $resp_content->data : null, 268 | \property_exists($resp_content, 'message') ? $resp_content->message : null, 269 | \property_exists($resp_content, 'error_code') ? $resp_content->error_code : null, 270 | $request_params, 271 | $request_data, 272 | $method 273 | ); 274 | } 275 | 276 | public function get_version() 277 | { 278 | return \file_get_contents(MAGIC_ADMIN_PHP_PATH . '/VERSION'); 279 | } 280 | 281 | public function _set_platform($platform) 282 | { 283 | $this->_platform = $platform; 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /tests/HttpClientTest.php: -------------------------------------------------------------------------------- 1 | requestsClient = new \MagicAdmin\HttpClient( 23 | $api_secret_key, 24 | $timeout, 25 | $retries, 26 | $backoff_factor 27 | ); 28 | 29 | $this->requestsClient->_set_platform($platform); 30 | } 31 | 32 | public function testRetrieves() 33 | { 34 | $timeout = 10; 35 | $retries = 3; 36 | $backoff_factor = 0.02; 37 | $api_secret_key = 'magic_admin'; 38 | 39 | static::assertSame($this->requestsClient->_timeout, $timeout); 40 | static::assertSame($this->requestsClient->_retries, $retries); 41 | static::assertSame($this->requestsClient->_backoff_factor, $backoff_factor); 42 | static::assertSame($this->requestsClient->_api_secret_key, $api_secret_key); 43 | } 44 | 45 | public function testGetVersion() 46 | { 47 | $mock = $this->createMock(\MagicAdmin\HttpClient::class); 48 | $mock->method('get_version')->willReturn('1.0.0'); 49 | 50 | static::assertSame($mock->get_version(), '1.0.0'); 51 | } 52 | 53 | public function testGetUserAgent() 54 | { 55 | $expected_array = []; 56 | $expected_array[] = 'language: php'; 57 | $expected_array[] = 'sdk_version: ' . $this->requestsClient->get_version(); 58 | $expected_array[] = 'publisher: Magic Labs Inc.'; 59 | $expected_array[] = 'http_lib: magic-admin-php'; 60 | $expected_array[] = 'platform: test_platform'; 61 | $expected_array[] = 'server_name: local_phpunit'; 62 | 63 | static::assertSame($this->requestsClient->_get_user_agent(), $expected_array); 64 | } 65 | 66 | public function testGetRequestHeaders() 67 | { 68 | $api_secret_key = 'magic_admin'; 69 | 70 | $mock = $this->createMock(\MagicAdmin\HttpClient::class); 71 | $mock->method('_get_user_agent')->willReturn( 72 | [ 73 | 'language: php', 74 | 'sdk_version: ' . $this->requestsClient->get_version(), 75 | 'publisher: Magic Labs Inc.', 76 | 'http_lib: magic-admin-php', 77 | 'platform: test_platform', 78 | 'server_name: local_phpunit', 79 | ] 80 | ); 81 | $expected_headers = []; 82 | $expected_headers[] = 'X-Magic-Secret-Key: ' . $api_secret_key; 83 | $expected_headers[] = 'User-Agent: ' . \json_encode($mock->_get_user_agent()); 84 | 85 | static::assertSame($this->requestsClient->_get_request_headers(), $expected_headers); 86 | } 87 | 88 | public function testParseAndConvertToApiResponse() 89 | { 90 | $resp_content = \json_encode(['data' => 'magic_admin']); 91 | $status_code = 200; 92 | $method = 'post'; 93 | $request_params = 'magic request_params'; 94 | $request_data = 'magic rquest_data'; 95 | 96 | $result = $this->requestsClient->_parse_and_convert_to_api_response( 97 | $resp_content, 98 | $status_code, 99 | $method, 100 | $request_params, 101 | $request_data 102 | ); 103 | 104 | static::assertSame($result->content->data, \json_decode($resp_content)->data); 105 | static::assertSame($result->status_code, $status_code); 106 | static::assertSame($result->data, \json_decode($resp_content)->data); 107 | } 108 | 109 | public function testCheckRetry() 110 | { 111 | // check with retry number 112 | static::assertSame($this->requestsClient->check_retry(null, 200, 3), false); 113 | // check with error 114 | static::assertSame($this->requestsClient->check_retry(CURLE_OPERATION_TIMEOUTED, 200, 1), true); 115 | static::assertSame($this->requestsClient->check_retry(CURLE_COULDNT_CONNECT, 200, 1), true); 116 | // check with http code 117 | static::assertSame($this->requestsClient->check_retry(null, 409, 1), true); 118 | static::assertSame($this->requestsClient->check_retry(null, 500, 1), true); 119 | static::assertSame($this->requestsClient->check_retry(null, 200, 1), false); 120 | } 121 | 122 | public function testPostNotFoundRequest() 123 | { 124 | $method = 'post'; 125 | $url = '/v2/admin/auth/user/path'; 126 | $params = 'params'; 127 | $data = 'data'; 128 | 129 | $mock = $this->createMock(\MagicAdmin\HttpClient::class); 130 | $mock->method('api_request')->with($method, $url, $params, $data)->willReturn( 131 | [ 132 | '{"data":{},"error_code":"NOT_FOUND","message":"The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.","status":"failed"}', 133 | 404, 134 | ] 135 | ); 136 | 137 | list($content, $status_code) = $mock->api_request($method, $url, $params, $data); 138 | 139 | $this->expectException(MagicAdmin\Exception\ApiException::class); 140 | 141 | $result = $this->requestsClient->_parse_and_convert_to_api_response( 142 | $content, 143 | $status_code, 144 | $method, 145 | $params, 146 | $data 147 | ); 148 | } 149 | 150 | public function testPostForbiddenRequest() 151 | { 152 | $method = 'post'; 153 | $url = '/path'; 154 | $params = 'params'; 155 | $data = 'data'; 156 | 157 | $mock = $this->createMock(\MagicAdmin\HttpClient::class); 158 | $mock->method('api_request')->with($method, $url, $params, $data)->willReturn( 159 | [ 160 | '{"data":{},"error_code":"UNAUTHORIZED","message":"Please try again.","status":"failed"}', 161 | 403, 162 | ] 163 | ); 164 | 165 | list($content, $status_code) = $mock->api_request($method, $url, $params, $data); 166 | 167 | $this->expectException(MagicAdmin\Exception\ForbiddenException::class); 168 | 169 | $result = $this->requestsClient->_parse_and_convert_to_api_response( 170 | $content, 171 | $status_code, 172 | $method, 173 | $params, 174 | $data 175 | ); 176 | } 177 | 178 | public function testPostUnauthorizedRequest() 179 | { 180 | $method = 'post'; 181 | $url = '/v2/admin/auth/user/logout'; 182 | $params = null; 183 | $data = ['issuer' => 'did:ethr:0xabA53bd22b2673C6c42ffA11C251B45D8CcBe4a4']; 184 | 185 | $mock = $this->createMock(\MagicAdmin\HttpClient::class); 186 | $mock->method('api_request')->with($method, $url, $params, $data)->willReturn( 187 | [ 188 | '{"data":{},"error_code":"UNAUTHORIZED","message":"Please try again.","status":"failed"}', 189 | 401, 190 | ] 191 | ); 192 | 193 | list($content, $status_code) = $mock->api_request($method, $url, $params, $data); 194 | 195 | $this->expectException(MagicAdmin\Exception\AuthenticationException::class); 196 | 197 | $result = $this->requestsClient->_parse_and_convert_to_api_response( 198 | $content, 199 | $status_code, 200 | $method, 201 | $params, 202 | $data 203 | ); 204 | } 205 | 206 | public function testPostTooManyRequest() 207 | { 208 | $method = 'post'; 209 | $url = '/v2/admin/auth/user/logout'; 210 | $params = null; 211 | $data = ['issuer' => 'did:ethr:0xabA53bd22b2673C6c42ffA11C251B45D8CcBe4a4']; 212 | 213 | $mock = $this->createMock(\MagicAdmin\HttpClient::class); 214 | $mock->method('api_request')->with($method, $url, $params, $data)->willReturn( 215 | [ 216 | '{"data":{},"error_code":"TOO_MANY_REQUEST","message":"Please try again.","status":"failed"}', 217 | 429, 218 | ] 219 | ); 220 | 221 | list($content, $status_code) = $mock->api_request($method, $url, $params, $data); 222 | 223 | $this->expectException(MagicAdmin\Exception\RateLimitingException::class); 224 | 225 | $result = $this->requestsClient->_parse_and_convert_to_api_response( 226 | $content, 227 | $status_code, 228 | $method, 229 | $params, 230 | $data 231 | ); 232 | } 233 | 234 | public function testPostInvalidKeyRequest() 235 | { 236 | $method = 'post'; 237 | $url = '/v2/admin/auth/user/logout'; 238 | $params = null; 239 | $data = ['issuer' => 'did:ethr:0xabA53bd22b2673C6c42ffA11C251B45D8CcBe4a4']; 240 | 241 | $mock = $this->createMock(\MagicAdmin\HttpClient::class); 242 | $mock->method('api_request')->with($method, $url, $params, $data)->willReturn( 243 | [ 244 | '{"data":{},"error_code":"INVALID_API_KEY","message":"Given API key is invalid. Please try again.","status":"failed"}', 245 | 400, 246 | ] 247 | ); 248 | 249 | list($content, $status_code) = $mock->api_request($method, $url, $params, $data); 250 | 251 | $this->expectException(MagicAdmin\Exception\BadRequestException::class); 252 | 253 | $result = $this->requestsClient->_parse_and_convert_to_api_response( 254 | $content, 255 | $status_code, 256 | $method, 257 | $params, 258 | $data 259 | ); 260 | } 261 | 262 | public function testGetInvalidKeyRequest() 263 | { 264 | $method = 'get'; 265 | $url = '/v1/admin/auth/user/get'; 266 | $params = ['issuer' => 'did:ethr:0xabA53bd22b2673C6c42ffA11C251B45D8CcBe4a4']; 267 | 268 | $mock = $this->createMock(\MagicAdmin\HttpClient::class); 269 | $mock->method('api_request')->with($method, $url, $params)->willReturn( 270 | [ 271 | '{"data":{},"error_code":"INVALID_API_KEY","message":"Given API key is invalid. Please try again.","status":"failed"}', 272 | 400, 273 | ] 274 | ); 275 | 276 | list($content, $status_code) = $mock->api_request($method, $url, $params); 277 | 278 | $this->expectException(MagicAdmin\Exception\BadRequestException::class); 279 | 280 | $result = $this->requestsClient->_parse_and_convert_to_api_response( 281 | $content, 282 | $status_code, 283 | $method, 284 | $params, 285 | null 286 | ); 287 | } 288 | 289 | public function testGetMalformedIssuerRequest() 290 | { 291 | $method = 'get'; 292 | $url = '/v1/admin/auth/user/get'; 293 | $params = ['issuer' => 'magic_admin']; 294 | 295 | $mock = $this->createMock(\MagicAdmin\HttpClient::class); 296 | $mock->method('api_request')->with($method, $url, $params)->willReturn( 297 | [ 298 | '{"data":{},"error_code":"MALFORMED_DID_ISSUER","message":"Given id (magic_admin) is malformed.","status":"failed"}', 299 | 400, 300 | ] 301 | ); 302 | 303 | list($content, $status_code) = $mock->api_request($method, $url, $params); 304 | 305 | $this->expectException(MagicAdmin\Exception\BadRequestException::class); 306 | 307 | $result = $this->requestsClient->_parse_and_convert_to_api_response( 308 | $content, 309 | $status_code, 310 | $method, 311 | $params, 312 | null 313 | ); 314 | } 315 | 316 | public function testGetGoodRequest() 317 | { 318 | $method = 'get'; 319 | $url = '/v1/admin/auth/user/get'; 320 | $params = ['issuer' => 'did:ethr:0xabA53bd22b2673C6c42ffA11C251B45D8CcBe4a4']; 321 | 322 | $mock = $this->createMock(\MagicAdmin\HttpClient::class); 323 | $mock->method('api_request')->with($method, $url, $params)->willReturn( 324 | [ 325 | '{"data":{"email":"test@user.com","issuer":"did:ethr:0xabA53bd22b2673C6c42ffA11C251B45D8CcBe4a4","public_address":"0xabA53bd22b2673C6c42ffA11C251B45D8CcBe4a4"},"error_code":"","message":"","status":"ok"}', 326 | 200, 327 | ] 328 | ); 329 | 330 | list($content, $status_code) = $mock->api_request($method, $url, $params); 331 | 332 | $result = $this->requestsClient->_parse_and_convert_to_api_response( 333 | $content, 334 | $status_code, 335 | $method, 336 | $params, 337 | null 338 | ); 339 | 340 | static::assertSame($result->data->issuer, 'did:ethr:0xabA53bd22b2673C6c42ffA11C251B45D8CcBe4a4'); 341 | static::assertSame($result->content->error_code, ''); 342 | static::assertSame($result->content->message, ''); 343 | static::assertSame($result->content->status, 'ok'); 344 | } 345 | } 346 | --------------------------------------------------------------------------------