├── 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 |
--------------------------------------------------------------------------------