├── .gitignore
├── .scrutinizer.yml
├── .travis.yml
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── composer.json
├── contributors
├── src
├── Grant
│ └── MiniProgram
│ │ └── AuthorizationCode.php
├── Provider
│ ├── MiniProgramProvider.php
│ ├── MiniProgramResourceOwner.php
│ ├── WebProvider.php
│ └── WebResourceOwner.php
├── Support
│ ├── Common
│ │ └── AESEncoder.php
│ └── MiniProgram
│ │ ├── MiniProgramDataCrypt.php
│ │ ├── PKCS7Encoder.php
│ │ └── demo.php
└── Token
│ └── MiniProgram
│ └── AccessToken.php
└── tests
└── src
├── Provider
├── MiniProgramProviderTest.php
└── WebProviderTest.php
├── Sample
├── .gitignore
├── config.ini.dist
└── web-sample.php
└── Support
└── MiniProgram
└── MiniProgramDataCryptTest.php
/.gitignore:
--------------------------------------------------------------------------------
1 | # Add any directories, files, or patterns you don't want to be tracked by version control
2 | .idea
3 | *.iml
4 | /build
5 | .DS_Store
6 | /vendor
7 | composer.lock
--------------------------------------------------------------------------------
/.scrutinizer.yml:
--------------------------------------------------------------------------------
1 | filter:
2 | paths: ["src/*"]
3 | excluded_paths:
4 | - "tests/"
5 | - "vendor/"
6 | checks:
7 | php:
8 | code_rating: true
9 | duplication: true
10 | remove_extra_empty_lines: true
11 | remove_php_closing_tag: true
12 | remove_trailing_whitespace: true
13 | fix_use_statements:
14 | remove_unused: true
15 | preserve_multiple: false
16 | preserve_blanklines: true
17 | order_alphabetically: true
18 | fix_php_opening_tag: true
19 | fix_linefeed: true
20 | fix_line_ending: true
21 | fix_identation_4spaces: true
22 | fix_doc_comments: true
23 | tools:
24 | external_code_coverage:
25 | timeout: 600
26 | runs: 3
27 | php_analyzer: true
28 | php_code_coverage: false
29 | php_code_sniffer:
30 | config:
31 | standard: PSR2
32 | filter:
33 | paths: ['src']
34 | php_loc:
35 | enabled: true
36 | excluded_dirs: [vendor, test]
37 | php_cpd:
38 | enabled: true
39 | excluded_dirs: [vendor, test]
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 |
3 | matrix:
4 | include:
5 | - php: 5.6
6 | - php: 7.0
7 | - php: 7.1
8 | - php: nightly
9 | - php: hhvm-3.6
10 | sudo: required
11 | dist: trusty
12 | group: edge
13 | - php: hhvm-3.9
14 | sudo: required
15 | dist: trusty
16 | group: edge
17 | - php: hhvm-3.12
18 | sudo: required
19 | dist: trusty
20 | group: edge
21 | - php: hhvm-3.15
22 | sudo: required
23 | dist: trusty
24 | group: edge
25 | - php: hhvm-nightly
26 | sudo: required
27 | dist: trusty
28 | group: edge
29 | fast_finish: true
30 | allow_failures:
31 | - php: nightly
32 | - php: hhvm-nightly
33 |
34 | before_script:
35 | - travis_retry composer self-update
36 | - travis_retry composer update --no-interaction --prefer-source --dev
37 | - travis_retry phpenv rehash
38 |
39 | script:
40 | - ./vendor/bin/phpunit --coverage-text --coverage-clover=coverage.clover tests
41 | - ./vendor/bin/phpcs --standard=psr2 -sp src/
42 |
43 | after_success:
44 | - wget https://scrutinizer-ci.com/ocular.phar
45 | - php ocular.phar code-coverage:upload --format=php-clover coverage.clover
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Contributions are **welcome** and will be fully **credited**.
4 |
5 | We accept contributions via Pull Requests on [Github](https://github.com/oakhope/oauth2-wechat).
6 |
7 |
8 | ## Pull Requests
9 |
10 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer).
11 |
12 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests.
13 |
14 | - **Document any change in behaviour** - Make sure the README and any other relevant documentation are kept up-to-date.
15 |
16 | - **Consider our release cycle** - We try to follow SemVer. Randomly breaking public APIs is not an option.
17 |
18 | - **Create topic branches** - Don't ask us to pull from your master branch.
19 |
20 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests.
21 |
22 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please squash them before submitting.
23 |
24 | - **Ensure tests pass!** - Please run the tests (see below) before submitting your pull request, and make sure they pass. We won't accept a patch until all tests pass.
25 |
26 | - **Ensure no coding standards violations** - Please run PHP Code Sniffer using the PSR-2 standard (see below) before submitting your pull request. A violation will cause the build to fail, so please make sure there are no violations. We can't accept a patch if the build fails.
27 |
28 |
29 | ## Running Tests
30 |
31 | ``` bash
32 | $ ./vendor/bin/phpunit
33 | ```
34 |
35 |
36 | ## Running PHP Code Sniffer
37 |
38 | ``` bash
39 | $ ./vendor/bin/phpcs src --standard=psr2 -sp
40 | ```
41 |
42 | **Happy coding**!
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017 Benji Wang
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Wechat Provider for OAuth 2.0 Client
2 |
3 | [](https://github.com/oakhope/oauth2-wechat/releases)
4 | [](LICENSE)
5 | [](https://travis-ci.org/oakhope/oauth2-wechat)
6 | [](https://scrutinizer-ci.com/g/oakhope/oauth2-wechat/code-structure)
7 | [](https://scrutinizer-ci.com/g/oakhope/oauth2-wechat)
8 | [](https://packagist.org/packages/oakhope/oauth2-wechat)
9 |
10 | This package provides Wechat OAuth 2.0 support for the PHP League's [OAuth 2.0 Client](https://github.com/thephpleague/oauth2-client).
11 |
12 | - DONE:
13 | > Website SDK, Mini Programs
14 |
15 | - TODO:
16 | > Mobile App SDK
17 |
18 | ## Installation
19 |
20 | To install, use composer:
21 |
22 | ```
23 | composer require oakhope/oauth2-wechat
24 | ```
25 |
26 | ## Usage
27 |
28 | Usage is the same as The League's OAuth client, using `\Oakhope\OAuth2\Client\Provider\{WebProvider}` as the provider.
29 |
30 | ### Authorization Code Flow
31 |
32 | ```php
33 | $provider = new \Oakhope\OAuth2\Client\Provider\WebProvider([
34 | 'appid' => '{wechat-client-id}',
35 | 'secret' => '{wechat-client-secret}',
36 | 'redirect_uri' => 'https://example.com/callback-url'
37 | ]);
38 |
39 | // If we don't have an authorization code then get one
40 | if (!isset($_GET['code'])) {
41 |
42 | // Fetch the authorization URL from the provider; this returns the
43 | // urlAuthorize option and generates and applies any necessary parameters
44 | // (e.g. state).
45 | $authorizationUrl = $provider->getAuthorizationUrl();
46 |
47 | // Get the state generated for you and store it to the session.
48 | $_SESSION['oauth2state'] = $provider->getState();
49 |
50 | // Redirect the user to the authorization URL.
51 | header('Location: '.$authorizationUrl);
52 | exit;
53 |
54 | // Check given state against previously stored one to mitigate CSRF attack
55 | } elseif (empty($_GET['state']) || ($_GET['state'] !== rtrim($_SESSION['oauth2state'], '#wechat_redirect'))) {
56 |
57 | unset($_SESSION['oauth2state']);
58 | exit('Invalid state');
59 |
60 | } else {
61 |
62 | try {
63 |
64 | // Try to get an access token using the authorization code grant.
65 | $accessToken = $provider->getAccessToken(
66 | 'authorization_code',
67 | [
68 | 'code' => $_GET['code'],
69 | ]);
70 |
71 | // We have an access token, which we may use in authenticated
72 | // requests against the service provider's API.
73 | echo "token: ".$accessToken->getToken()."
";
74 | echo "refreshToken: ".$accessToken->getRefreshToken()."
";
75 | echo "Expires: ".$accessToken->getExpires()."
";
76 | echo ($accessToken->hasExpired() ? 'expired' : 'not expired')."
";
77 |
78 | // Using the access token, we may look up details about the
79 | // resource owner.
80 | $resourceOwner = $provider->getResourceOwner($accessToken);
81 |
82 | var_export($resourceOwner->toArray());
83 |
84 | } catch (\League\OAuth2\Client\Provider\Exception\IdentityProviderException $e) {
85 |
86 | // Failed to get the access token or user details.
87 | echo "error:";
88 | exit($e->getMessage());
89 | }
90 | }
91 | ```
92 |
93 |
94 | ### Refreshing a Token
95 |
96 | Once your application is authorized, you can refresh an expired token using a refresh token rather than going through the entire process of obtaining a brand new token. To do so, simply reuse this refresh token from your data store to request a refresh.
97 |
98 | _This example uses [Brent Shaffer's](https://github.com/bshaffer) demo OAuth 2.0 application named **Lock'd In**. See authorization code example above, for more details._
99 |
100 | ```php
101 | $provider = new \Oakhope\OAuth2\Client\Provider\WebProvider([
102 | 'appid' => '{wechat-client-id}',
103 | 'secret' => '{wechat-client-secret}',
104 | 'redirect_uri' => 'https://example.com/callback-url'
105 | ]);
106 |
107 | $existingAccessToken = getAccessTokenFromYourDataStore();
108 |
109 | if ($existingAccessToken->hasExpired()) {
110 | $newAccessToken = $provider->getAccessToken('refresh_token', [
111 | 'refresh_token' => $existingAccessToken->getRefreshToken()
112 | ]);
113 |
114 | // Purge old access token and store new access token to your data store.
115 | }
116 | ```
117 |
118 | ## Testing
119 |
120 | ``` bash
121 | $ ./vendor/bin/phpunit --colors tests
122 | ```
123 |
124 | ## Contributing
125 |
126 | Please see [CONTRIBUTING](https://github.com/oakhope/oauth2-wechat/blob/master/CONTRIBUTING.md) for details.
127 |
128 |
129 | ## Credits
130 |
131 | - [Benji Wang](https://github.com/oakhope)
132 | - [All Contributors](https://github.com/oakhope/oauth2-wechat/contributors)
133 |
134 |
135 | ## License
136 |
137 | The MIT License (MIT). Please see [License File](https://github.com/oakhope/oauth2-wechat/blob/master/LICENSE) for more information.
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "oakhope/oauth2-wechat",
3 | "type": "library",
4 | "description": "微信登录认证授权 Wechat login authorization. This package provides Wechat OAuth 2.0 support for the PHP League's OAuth 2.0 Client",
5 | "authors": [
6 | {
7 | "name": "Benji Wang",
8 | "email": "oak.hope@gmail.com"
9 | }
10 | ],
11 | "keywords": [
12 | "wechat",
13 | "weixin",
14 | "wechat login",
15 | "weixin login",
16 | "oauth2 wechat",
17 | "oauth2 weixin",
18 | "oauth",
19 | "oauth2",
20 | "client",
21 | "authorization",
22 | "authorisation"
23 | ],
24 | "homepage": "https://github.com/oakhope/oauth2-wechat",
25 | "license": "MIT",
26 | "require": {
27 | "league/oauth2-client": "^2.2"
28 | },
29 | "require-dev": {
30 | "phpunit/phpunit": "~4.0",
31 | "mockery/mockery": "~0.9",
32 | "squizlabs/php_codesniffer": "~2.0"
33 | },
34 | "suggest": {
35 | "symfony/var-dumper": "^3.3"
36 | },
37 | "autoload": {
38 | "psr-4": {
39 | "Oakhope\\OAuth2\\Client\\": "src/"
40 | }
41 | },
42 | "autoload-dev": {
43 | "psr-4": {
44 | "Oakhope\\OAuth2\\Client\\Test\\": "tests/src/"
45 | }
46 | },
47 | "extra": {
48 | "branch-alias": {
49 | "dev-master": "1.0.x-dev"
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/contributors:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oakhope/oauth2-wechat/04f18ee6283e4d1f2a2ac998b99451fd09455e12/contributors
--------------------------------------------------------------------------------
/src/Grant/MiniProgram/AuthorizationCode.php:
--------------------------------------------------------------------------------
1 | checkRequiredParameters(
40 | [
41 | 'appid',
42 | 'secret',
43 | ],
44 | $options
45 | );
46 |
47 | $options['access_token'] = 'js_code';
48 |
49 | parent::__construct($options, $collaborators);
50 | }
51 |
52 | /**
53 | * Returns the base URL for authorizing a client.
54 | *
55 | * Eg. https://oauth.service.com/authorize
56 | *
57 | * @return string
58 | */
59 | public function getBaseAuthorizationUrl()
60 | {
61 | throw new \LogicException('use wx.login(OBJECT) to get js_code');
62 | }
63 |
64 | /**
65 | * Returns the base URL for requesting an access token.
66 | *
67 | * Eg. https://oauth.service.com/token
68 | *
69 | * @param array $params
70 | * @return string
71 | */
72 | public function getBaseAccessTokenUrl(array $params)
73 | {
74 | return 'https://api.weixin.qq.com/sns/jscode2session';
75 | }
76 |
77 | /**
78 | * Requests an access token using a specified grant and option set.
79 | *
80 | * @param string $jsCode
81 | * @param array $options
82 | * @return AccessToken
83 | */
84 | public function getAccessToken($jsCode, array $options = [])
85 | {
86 | $this->jscode = $jsCode;
87 | $grant = new AuthorizationCode();
88 | $grant = $this->verifyGrant($grant);
89 | $params = [
90 | 'appid' => $this->appid,
91 | 'secret' => $this->secret,
92 | 'js_code' => $jsCode,
93 | ];
94 |
95 | $params = $grant->prepareRequestParameters($params, $options);
96 | $request = $this->getAccessTokenRequest($params);
97 | $response = $this->getParsedResponse($request);
98 | $prepared = $this->prepareAccessTokenResponse($response);
99 | $token = $this->createAccessToken($prepared, $grant);
100 |
101 | return $token;
102 | }
103 |
104 | /**
105 | * Creates an access token from a response.
106 | *
107 | * The grant that was used to fetch the response can be used to provide
108 | * additional context.
109 | *
110 | * @param array $response
111 | * @param AbstractGrant $grant
112 | * @return AccessToken
113 | */
114 | protected function createAccessToken(array $response, AbstractGrant $grant)
115 | {
116 | return new \Oakhope\OAuth2\Client\Token\MiniProgram\AccessToken(
117 | $response
118 | );
119 | }
120 |
121 | /**
122 | * Returns the URL for requesting the resource owner's details.
123 | *
124 | * @param AccessToken $token
125 | * @return string
126 | */
127 | public function getResourceOwnerDetailsUrl(AccessToken $token)
128 | {
129 | throw new \LogicException(
130 | 'use wx.getUserInfo(OBJECT) to get ResourceOwnerDetails'
131 | );
132 | }
133 |
134 | /**
135 | * Returns the default scopes used by this provider.
136 | *
137 | * This should only be the scopes that are required to request the details
138 | * of the resource owner, rather than all the available scopes.
139 | *
140 | * @return array
141 | */
142 | protected function getDefaultScopes()
143 | {
144 | return [];
145 | }
146 |
147 | /**
148 | * Checks a provider response for errors.
149 | *
150 | * @throws IdentityProviderException
151 | * @param ResponseInterface $response
152 | * @param array|string $data Parsed response data
153 | * @return void
154 | */
155 | protected function checkResponse(ResponseInterface $response, $data)
156 | {
157 | $errcode = $this->getValueByKey($data, 'errcode');
158 | $errmsg = $this->getValueByKey($data, 'errmsg');
159 |
160 | if ($errcode || $errmsg) {
161 | throw new IdentityProviderException($errmsg, $errcode, $response);
162 | };
163 | }
164 |
165 | /**
166 | * Generates a resource owner object from a successful resource owner
167 | * details request.
168 | *
169 | * @param array $response
170 | * @param AccessToken $token
171 | * @return ResourceOwnerInterface
172 | */
173 | public function createResourceOwner(array $response, AccessToken $token)
174 | {
175 | return new MiniProgramResourceOwner($response, $token, $this->appid);
176 | }
177 |
178 | /**
179 | * Requests and returns the resource owner of given access token.
180 | *
181 | * @param AccessToken $token
182 | * @return ResourceOwnerInterface
183 | */
184 | public function getResourceOwner(AccessToken $token)
185 | {
186 | if (null == $this->responseUserInfo) {
187 | throw new \InvalidArgumentException(
188 | "setResponseUserInfo by wx.getUserInfo(OBJECT)'s response data first"
189 | );
190 | }
191 |
192 | return $this->createResourceOwner($this->responseUserInfo, $token);
193 | }
194 |
195 | /**
196 | * set by wx.getUserInfo(OBJECT)'s response data
197 | *
198 | * @param array $response
199 | */
200 | public function setResponseUserInfo($response)
201 | {
202 | $this->responseUserInfo = $response;
203 | }
204 | }
205 |
--------------------------------------------------------------------------------
/src/Provider/MiniProgramResourceOwner.php:
--------------------------------------------------------------------------------
1 | checkSignature($response, $token);
23 | $this->responseUserInfo = $response;
24 | $this->token = $token;
25 | $this->appid = $appid;
26 |
27 | if (!empty($response['encryptedData'])) {
28 | $this->decryptData = $this->decrypt();
29 | }
30 | }
31 |
32 |
33 | /**
34 | * @param $response
35 | * @param AccessToken $token
36 | * @throws \Exception
37 | */
38 | private function checkSignature($response, $token)
39 | {
40 | if ($response['signature'] !== sha1(
41 | $response['rawData'].$token->getSessionKey()
42 | )) {
43 | throw new IdentityProviderException('signature error', 0, $response);
44 | }
45 | }
46 |
47 | /**
48 | * @return mixed
49 | * @throws \Exception
50 | */
51 | private function decrypt()
52 | {
53 | $dataCrypt = new MiniProgramDataCrypt(
54 | $this->appid,
55 | $this->token->getSessionKey()
56 | );
57 | $errCode = $dataCrypt->decryptData(
58 | $this->responseUserInfo['encryptedData'],
59 | $this->responseUserInfo['iv'],
60 | $data
61 | );
62 |
63 | if ($errCode == 0) {
64 | return $data;
65 | } else {
66 | throw new IdentityProviderException('decrypt error', $errCode, $this->responseUserInfo);
67 | }
68 | }
69 |
70 | /**
71 | * Returns the identifier of the authorized resource owner.
72 | *
73 | * @return mixed
74 | */
75 | public function getId()
76 | {
77 | return $this->decryptData ? $this->decryptData['openid'] : null;
78 | }
79 |
80 | public function getDecryptData()
81 | {
82 | return $this->decryptData;
83 | }
84 |
85 | public function getResponseUserInfo()
86 | {
87 | return $this->responseUserInfo;
88 | }
89 |
90 | /**
91 | * Return all of the owner details available as an array.
92 | *
93 | * @return array
94 | */
95 | public function toArray()
96 | {
97 | return $this->token->getValues();
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/src/Provider/WebProvider.php:
--------------------------------------------------------------------------------
1 | $this->appid
42 | ];
43 |
44 | if (!isset($options['redirect_uri'])) {
45 | $options['redirect_uri'] = $this->redirect_uri;
46 | }
47 |
48 | $options += [
49 | 'response_type' => 'code'
50 | ];
51 |
52 | if (empty($options['scope'])) {
53 | $options['scope'] = 'snsapi_login';
54 | }
55 |
56 | if (is_array($options['scope'])) {
57 | $separator = $this->getScopeSeparator();
58 | $options['scope'] = implode($separator, $options['scope']);
59 | }
60 |
61 | if (empty($options['state'])) {
62 | $options['state'] = $this->getRandomState().'#wechat_redirect';
63 | }
64 |
65 | // Store the state as it may need to be accessed later on.
66 | $this->state = $options['state'];
67 |
68 | return $options;
69 | }
70 |
71 | /**
72 | * Returns the base URL for requesting an access token.
73 | *
74 | * @param array $params
75 | * @return string
76 | */
77 | public function getBaseAccessTokenUrl(array $params)
78 | {
79 | return 'https://api.weixin.qq.com/sns/oauth2/access_token';
80 | }
81 |
82 | /**
83 | * Requests an access token using a specified grant and option set.
84 | *
85 | * @param mixed $grant
86 | * @param array $options
87 | * @return AccessToken
88 | */
89 | public function getAccessToken($grant, array $options = [])
90 | {
91 | $grant = $this->verifyGrant($grant);
92 | $params = [
93 | 'appid' => $this->appid,
94 | 'secret' => $this->secret
95 | ];
96 |
97 | $params = $grant->prepareRequestParameters($params, $options);
98 | $request = $this->getAccessTokenRequest($params);
99 | $response = $this->getParsedResponse($request);
100 | $prepared = $this->prepareAccessTokenResponse($response);
101 | $token = $this->createAccessToken($prepared, $grant);
102 |
103 | return $token;
104 | }
105 |
106 | /**
107 | * Creates an access token from a response.
108 | *
109 | * The grant that was used to fetch the response can be used to provide
110 | * additional context.
111 | *
112 | * @param array $response
113 | * @param AbstractGrant $grant
114 | * @return AccessToken
115 | */
116 | protected function createAccessToken(array $response, AbstractGrant $grant)
117 | {
118 | return new AccessToken($response);
119 | }
120 |
121 | /**
122 | * Returns the URL for requesting the resource owner's details.
123 | *
124 | * @param AccessToken $token
125 | * @return string
126 | */
127 | public function getResourceOwnerDetailsUrl(AccessToken $token)
128 | {
129 | return 'https://api.weixin.qq.com/sns/userinfo?access_token='.
130 | $token->getToken().'&openid='.$token->getValues()['openid'];
131 | }
132 |
133 | /**
134 | * Returns the default scopes used by this provider.
135 | *
136 | * This should only be the scopes that are required to request the details
137 | * of the resource owner, rather than all the available scopes.
138 | *
139 | * @return array
140 | */
141 | protected function getDefaultScopes()
142 | {
143 | return ['snsapi_userinfo'];
144 | }
145 |
146 | /**
147 | * Checks a provider response for errors.
148 | *
149 | * @throws IdentityProviderException
150 | * @param ResponseInterface $response
151 | * @param array|string|\Psr\Http\Message\ResponseInterface $data Parsed response data
152 | * @return void
153 | */
154 | protected function checkResponse(ResponseInterface $response, $data)
155 | {
156 | $errcode = $this->getValueByKey($data, 'errcode');
157 | $errmsg = $this->getValueByKey($data, 'errmsg');
158 |
159 | if ($errcode || $errmsg) {
160 | throw new IdentityProviderException($errmsg, $errcode, $response);
161 | };
162 | }
163 |
164 | /**
165 | * Generates a resource owner object from a successful resource owner
166 | * details request.
167 | *
168 | * @param array $response
169 | * @param AccessToken $token
170 | * @return ResourceOwnerInterface
171 | */
172 | protected function createResourceOwner(array $response, AccessToken $token)
173 | {
174 | return new WebResourceOwner($response);
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/src/Provider/WebResourceOwner.php:
--------------------------------------------------------------------------------
1 | response = $response;
25 | }
26 |
27 | /**
28 | * 普通用户的标识,对当前开发者帐号唯一
29 | *
30 | * @return string|null
31 | */
32 | public function getId()
33 | {
34 | return $this->response['openid'] ?: null;
35 | }
36 |
37 | /**
38 | * 普通用户昵称
39 | *
40 | * @return string|null
41 | */
42 | public function getNickname()
43 | {
44 | return $this->response['nickname'] ?: null;
45 | }
46 |
47 | /**
48 | * 普通用户性别,1为男性,2为女性
49 | *
50 | * @return string|null
51 | */
52 | public function getSex()
53 | {
54 | return $this->response['sex'] ?: null;
55 | }
56 |
57 | /**
58 | * 普通用户个人资料填写的省份
59 | *
60 | * @return string|null
61 | */
62 | public function getProvince()
63 | {
64 | return $this->response['province'] ?: null;
65 | }
66 |
67 | /**
68 | * 普通用户个人资料填写的城市
69 | *
70 | * @return string|null
71 | */
72 | public function getCity()
73 | {
74 | return $this->response['city'] ?: null;
75 | }
76 |
77 | /**
78 | * 国家,如中国为CN
79 | *
80 | * @return string|null
81 | */
82 | public function getCountry()
83 | {
84 | return $this->response['country'] ?: null;
85 | }
86 |
87 | /**
88 | * 用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像),用户没有头像时该项为空
89 | *
90 | * @return string|null
91 | */
92 | public function getHeadImgUrl()
93 | {
94 | return $this->response['headimgurl'] ?: null;
95 | }
96 |
97 | /**
98 | * 用户特权信息,json数组,如微信沃卡用户为(chinaunicom)
99 | *
100 | * @return string|null
101 | */
102 | public function getPrivilege()
103 | {
104 | return $this->response['privilege'] ?: null;
105 | }
106 |
107 | /**
108 | * 用户统一标识。针对一个微信开放平台帐号下的应用,同一用户的unionid是唯一的
109 | *
110 | * @return string|null
111 | */
112 | public function getUnionId()
113 | {
114 | return $this->response['unionid'] ?: null;
115 | }
116 |
117 | /**
118 | * Return all of the owner details available as an array.
119 | *
120 | * @return array
121 | */
122 | public function toArray()
123 | {
124 | return $this->response;
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/src/Support/Common/AESEncoder.php:
--------------------------------------------------------------------------------
1 | sessionKey = $sessionKey;
30 | $this->appid = $appid;
31 | }
32 |
33 |
34 | /**
35 | * 检验数据的真实性,并且获取解密后的明文.
36 | * @param $encryptedData string 加密的用户数据
37 | * @param $iv string 与用户数据一同返回的初始向量
38 | * @param $data string 解密后的原文
39 | *
40 | * @return int 成功0,失败返回对应的错误码
41 | */
42 | public function decryptData($encryptedData, $iv, &$data)
43 | {
44 | if (strlen($this->sessionKey) != 24) {
45 | return self::ILLEGAL_AES_KEY;
46 | }
47 |
48 | if (strlen($iv) != 24) {
49 | return self::ILLEGAL_IV;
50 | }
51 |
52 | $encoder = new PKCS7Encoder();
53 | $result = $encoder->decrypt($encryptedData, $this->sessionKey, $iv);
54 |
55 | if ($result[0] != 0) {
56 | return $result[0];
57 | }
58 |
59 | if ($result[1] == null) {
60 | return self::ILLEGAL_BUFFER;
61 | }
62 | if ($result[1]->watermark->appid != $this->appid) {
63 | return self::ILLEGAL_BUFFER;
64 | }
65 | $data = $result[1];
66 |
67 | return self::OK;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/Support/MiniProgram/PKCS7Encoder.php:
--------------------------------------------------------------------------------
1 | decode($decrypted);
35 |
36 | return array(0, json_decode($result));
37 | }
38 |
39 | /**
40 | * 对需要加密的明文进行填充补位
41 | *
42 | * @param string $text 需要进行填充补位操作的明文
43 | * @return string 补齐明文字符串
44 | */
45 | private function encode($text)
46 | {
47 | $text_length = strlen($text);
48 | // 计算需要填充的位数
49 | $amount_to_pad = PKCS7Encoder::BLOCK_SIZE - ($text_length % PKCS7Encoder::BLOCK_SIZE);
50 | if ($amount_to_pad == 0) {
51 | $amount_to_pad = PKCS7Encoder::BLOCK_SIZE;
52 | }
53 | // 获得补位所用的字符
54 | $pad_chr = chr($amount_to_pad);
55 | $tmp = "";
56 | for ($index = 0; $index < $amount_to_pad; $index++) {
57 | $tmp .= $pad_chr;
58 | }
59 |
60 | return $text.$tmp;
61 | }
62 |
63 | /**
64 | * 对解密后的明文进行补位删除
65 | *
66 | * @param string $text 解密后的明文
67 | * @return bool|string 删除填充补位后的明文
68 | */
69 | private function decode($text)
70 | {
71 | $pad = ord(substr($text, -1));
72 | if ($pad < 1 || $pad > 32) {
73 | $pad = 0;
74 | }
75 |
76 | return substr($text, 0, (strlen($text) - $pad));
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/Support/MiniProgram/demo.php:
--------------------------------------------------------------------------------
1 | decryptData($encryptedData, $iv, $data);
32 |
33 | if ($errCode == 0) {
34 | var_dump($data);
35 | } else {
36 | var_dump($errCode);
37 | }
38 |
--------------------------------------------------------------------------------
/src/Token/MiniProgram/AccessToken.php:
--------------------------------------------------------------------------------
1 | sessionKey = $options['session_key'];
46 |
47 | if (!empty($options['openid'])) {
48 | $this->openId = $options['openid'];
49 | }
50 |
51 | if (!empty($options['unionid'])) {
52 | $this->unionId = $options['unionid'];
53 | }
54 |
55 | // Capture any additional values that might exist in the token but are
56 | // not part of the standard response. Vendors will sometimes pass
57 | // additional user data this way.
58 | $this->values = array_diff_key($options, array_flip([
59 | 'session_key',
60 | 'openid',
61 | 'unionid'
62 | ]));
63 | }
64 |
65 | /**
66 | * Returns the session key string of this instance.
67 | *
68 | * @return string
69 | */
70 | public function getSessionKey()
71 | {
72 | return $this->sessionKey;
73 | }
74 |
75 | /**
76 | * Returns the resource owner identifier, if defined.
77 | *
78 | * @return string|null
79 | */
80 | public function getOpenId()
81 | {
82 | return $this->openId;
83 | }
84 |
85 | /**
86 | * Returns the resource owner identifier, if defined.
87 | *
88 | * @return string|null
89 | */
90 | public function getUnionId()
91 | {
92 | return $this->unionId;
93 | }
94 |
95 | /**
96 | * Returns additional vendor values stored in the token.
97 | *
98 | * @return array
99 | */
100 | public function getValues()
101 | {
102 | return $this->values;
103 | }
104 |
105 | /**
106 | * Returns the token key.
107 | *
108 | * @return string
109 | */
110 | public function __toString()
111 | {
112 | return (string) $this->getSessionKey();
113 | }
114 |
115 | /**
116 | * Returns an array of parameters to serialize when this is serialized with
117 | * json_encode().
118 | *
119 | * @return array
120 | */
121 | public function jsonSerialize()
122 | {
123 | $parameters = $this->values;
124 |
125 | if ($this->sessionKey) {
126 | $parameters['sessionKey'] = $this->sessionKey;
127 | }
128 |
129 | if ($this->openId) {
130 | $parameters['openid'] = $this->openId;
131 | }
132 |
133 | if ($this->unionId) {
134 | $parameters['unionid'] = $this->unionId;
135 | }
136 |
137 | return $parameters;
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/tests/src/Provider/MiniProgramProviderTest.php:
--------------------------------------------------------------------------------
1 | appid = 'wx4f4bc4dec97d474b';
28 | $this->sessionKey = 'tiihtNczf5v6AKRyjwEUhQ==';
29 | $this->encryptedData = "CiyLU1Aw2KjvrjMdj8YKliAjtP4gsMZMQmRzooG2xrDcvSnxIMXFufNstNGTyaGS9uT5geRa0W4oTOb1WT7fJlAC+oNPdbB+3hVbJSRgv+4lGOETKUQz6OYStslQ142dNCuabNPGBzlooOmB231qMM85d2/fV6ChevvXvQP8Hkue1poOFtnEtpyxVLW1zAo6/1Xx1COxFvrc2d7UL/lmHInNlxuacJXwu0fjpXfz/YqYzBIBzD6WUfTIF9GRHpOn/Hz7saL8xz+W//FRAUid1OksQaQx4CMs8LOddcQhULW4ucetDf96JcR3g0gfRK4PC7E/r7Z6xNrXd2UIeorGj5Ef7b1pJAYB6Y5anaHqZ9J6nKEBvB4DnNLIVWSgARns/8wR2SiRS7MNACwTyrGvt9ts8p12PKFdlqYTopNHR1Vf7XjfhQlVsAJdNiKdYmYVoKlaRv85IfVunYzO0IKXsyl7JCUjCpoG20f0a04COwfneQAGGwd5oa+T8yO5hzuyDb/XcxxmK01EpqOyuxINew==";
30 | $this->iv = 'r7BXXKkLb8qrSNn05n0qiA==';
31 |
32 | $this->provider = new MiniProgramProvider([
33 | 'appid' => $this->appid,
34 | 'secret' => 'appsecret',
35 | 'js_code' => 'JSCODE',
36 | ]);
37 |
38 | }
39 |
40 | public function tearDown()
41 | {
42 | m::close();
43 | parent::tearDown();
44 | }
45 |
46 | public function testAuthorizationUrl()
47 | {
48 | try {
49 | $url = $this->provider->getAuthorizationUrl();
50 | } catch (\Exception $e) {
51 | $this->assertInstanceOf('\LogicException', $e);
52 | $this->assertEquals('use wx.login(OBJECT) to get js_code',
53 | $e->getMessage());
54 | }
55 | }
56 |
57 | public function testGetResourceOwnerDetailsUrl()
58 | {
59 | try {
60 | $url = $this->provider->getResourceOwnerDetailsUrl(m::mock('League\OAuth2\Client\Token\AccessToken'));
61 | } catch (\Exception $e) {
62 | $this->assertInstanceOf('\LogicException', $e);
63 | $this->assertEquals('use wx.getUserInfo(OBJECT) to get ResourceOwnerDetails',
64 | $e->getMessage());
65 | }
66 | }
67 |
68 | public function testGetBaseAccessTokenUrl()
69 | {
70 | $params = [];
71 |
72 | $url = $this->provider->getBaseAccessTokenUrl($params);
73 | $uri = parse_url($url);
74 |
75 | $this->assertEquals('/sns/jscode2session', $uri['path']);
76 | }
77 |
78 | public function testGetAccessToken()
79 | {
80 | $response = m::mock('Psr\Http\Message\ResponseInterface');
81 | $response->shouldReceive('getBody')->andReturn('{"openid": "OPENID","session_key": "SESSIONKEY","unionid": "UNIONID"}');
82 | $response->shouldReceive('getHeader')->andReturn(['content-type' => 'json']);
83 |
84 | $client = m::mock('GuzzleHttp\ClientInterface');
85 | $client->shouldReceive('send')->times(1)->andReturn($response);
86 | $this->provider->setHttpClient($client);
87 |
88 | /** @var AccessToken $token */
89 | $token = $this->provider->getAccessToken('authorization_code', ['js_code' => 'mock_js_code']);
90 |
91 | $this->assertEquals('SESSIONKEY', $token->getSessionKey());
92 | $this->assertEquals('OPENID', $token->getOpenId());
93 | $this->assertEquals('UNIONID', $token->getUnionId());
94 | }
95 |
96 | public function testUserData()
97 | {
98 | $openid = 'oGZUI0egBJY1zhBYw2KhdUfwVJJE';
99 | $nickname = 'Band';
100 | $sex = 1;
101 | $language = 'zh_CN';
102 | $province = 'Guangdong';
103 | $city = 'Guangzhou';
104 | $country = 'CN';
105 | $headImagurl = 'http://wx.qlogo.cn/mmopen/vi_32/aSKcBBPpibyKNicHNTMM0qJVh8Kjgiak2AHWr8MHM4WgMEm7GFhsf8OYrySdbvAMvTsw3mo8ibKicsnfN5pRjl1p8HQ/0';
106 | $unionid = 'ocMvos6NjeKLIBqg5Mr9QjxrP1FA';
107 | $timestamp = '1477314187';
108 |
109 | $rawData = 'RAWDATA';
110 | $signature = sha1($rawData.$this->sessionKey);
111 | $userInfo = \GuzzleHttp\json_encode(['userinfo'=>'userinfo']);
112 |
113 | $postResponse = m::mock('Psr\Http\Message\ResponseInterface');
114 | $postResponse->shouldReceive('getBody')->andReturn('{"openid": "'.$openid.'","session_key": "'.$this->sessionKey.'","unionid": "'.$unionid.'"}');
115 | $postResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'json']);
116 |
117 |
118 | $userResponse = m::mock('Psr\Http\Message\ResponseInterface');
119 | $userResponse->shouldReceive('getBody')->andReturn('{"userInfo": '.$userInfo.',"rawData": "'.$rawData.'","signature": "'.$signature.'","encryptedData": "'.$this->encryptedData.'","iv": "'.$this->iv.'"}');
120 | $userResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'json']);
121 |
122 |
123 | $client = m::mock('GuzzleHttp\ClientInterface');
124 | $client->shouldReceive('send')
125 | ->times(1)
126 | ->andReturn($postResponse, $userResponse);
127 | $this->provider->setHttpClient($client);
128 |
129 | $token = $this->provider->getAccessToken('authorization_code', ['js_code' => 'mock_authorization_code']);
130 |
131 | $this->provider->setResponseUserInfo((array)\GuzzleHttp\json_decode($userResponse->getBody()));
132 | /** @var MiniProgramResourceOwner $user */
133 | $user = $this->provider->getResourceOwner($token);
134 |
135 | $this->assertEquals(\GuzzleHttp\json_decode($userInfo), $user->getResponseUserInfo()['userInfo']);
136 | $this->assertEquals($openid, $user->getDecryptData()->openId);
137 | $this->assertEquals($nickname, $user->getDecryptData()->nickName);
138 | $this->assertEquals($sex, $user->getDecryptData()->gender);
139 | $this->assertEquals($province, $user->getDecryptData()->province);
140 | $this->assertEquals($city, $user->getDecryptData()->city);
141 | $this->assertEquals($country, $user->getDecryptData()->country);
142 | $this->assertEquals($headImagurl, $user->getDecryptData()->avatarUrl);
143 | $this->assertEquals($unionid, $user->getDecryptData()->unionId);
144 | $this->assertEquals($language, $user->getDecryptData()->language);
145 | $this->assertEquals($timestamp, $user->getDecryptData()->watermark->timestamp);
146 | $this->assertEquals($this->appid, $user->getDecryptData()->watermark->appid);
147 | }
148 |
149 | public function testUserDataFails()
150 | {
151 | $errorPayloads = [
152 | '{"errcode":40029,"errmsg": "invalid code"}',
153 | '{"openid": "OPENID","session_key": "SESSIONKEY","unionid": "UNIONID"}'
154 | ];
155 |
156 | $testPayload = function ($payload) {
157 | $userResponse = m::mock('Psr\Http\Message\ResponseInterface');
158 | $userResponse->shouldReceive('getBody')->andReturn($payload);
159 | $userResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'json']);
160 | $userResponse->shouldReceive('getStatusCode')->andReturn(500);
161 |
162 | $client = m::mock('GuzzleHttp\ClientInterface');
163 | $client->shouldReceive('send')
164 | ->times(1)
165 | ->andReturn($userResponse);
166 | $this->provider->setHttpClient($client);
167 |
168 | try {
169 | $token = $this->provider->getAccessToken('authorization_code', ['js_code' => 'mock_authorization_code']);
170 | return false;
171 | } catch (\Exception $e) {
172 | $this->assertInstanceOf('\League\OAuth2\Client\Provider\Exception\IdentityProviderException', $e);
173 | }
174 |
175 | return $payload;
176 | };
177 |
178 | $this->assertCount(1, array_filter(array_map($testPayload, $errorPayloads)));
179 | }
180 | }
--------------------------------------------------------------------------------
/tests/src/Provider/WebProviderTest.php:
--------------------------------------------------------------------------------
1 | provider = new WebProvider([
20 | 'appid' => 'YOU_APPID',
21 | 'secret' => 'appsecret',
22 | 'redirect_uri' => 'http://example.com/your-redirect-url/',
23 | ]);
24 | }
25 |
26 | public function tearDown()
27 | {
28 | m::close();
29 | parent::tearDown();
30 | }
31 |
32 | public function testAuthorizationUrl()
33 | {
34 | $url = $this->provider->getAuthorizationUrl();
35 | $uri = parse_url($url);
36 | parse_str($uri['query'], $query);
37 | $this->assertArrayHasKey('appid', $query);
38 | $this->assertArrayHasKey('redirect_uri', $query);
39 | $this->assertArrayHasKey('response_type', $query);
40 | $this->assertArrayHasKey('scope', $query);
41 | $this->assertArrayHasKey('state', $query);
42 | $this->assertNotNull($this->provider->getState());
43 | }
44 |
45 | public function testScopes()
46 | {
47 | $scopeSeparator = ',';
48 | $options = ['scope' => [uniqid(), uniqid()]];
49 | $query = ['scope' => implode($scopeSeparator, $options['scope'])];
50 | $url = $this->provider->getAuthorizationUrl($options);
51 | $encodedScope = $this->buildQueryString($query);
52 | $this->assertContains($encodedScope, $url);
53 | }
54 |
55 | public function testGetAuthorizationUrl()
56 | {
57 | $url = $this->provider->getAuthorizationUrl();
58 | $uri = parse_url($url);
59 |
60 | $this->assertEquals('/connect/qrconnect', $uri['path']);
61 | }
62 |
63 | public function testGetBaseAccessTokenUrl()
64 | {
65 | $params = [];
66 |
67 | $url = $this->provider->getBaseAccessTokenUrl($params);
68 | $uri = parse_url($url);
69 |
70 | $this->assertEquals('/sns/oauth2/access_token', $uri['path']);
71 | }
72 |
73 | public function testGetAccessToken()
74 | {
75 | $response = m::mock('Psr\Http\Message\ResponseInterface');
76 | $response->shouldReceive('getBody')->andReturn('{"access_token": "ACCESS_TOKEN","expires_in": 7200,"refresh_token": "REFRESH_TOKEN","openid": "OPENID","scope": "SCOPE","unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"}');
77 | $response->shouldReceive('getHeader')->andReturn(['content-type' => 'json']);
78 |
79 | $client = m::mock('GuzzleHttp\ClientInterface');
80 | $client->shouldReceive('send')->times(1)->andReturn($response);
81 | $this->provider->setHttpClient($client);
82 |
83 | $token = $this->provider->getAccessToken('authorization_code', ['code' => 'mock_authorization_code']);
84 |
85 | $this->assertEquals('ACCESS_TOKEN', $token->getToken());
86 | $this->assertLessThanOrEqual(time() + 7200, $token->getExpires());
87 | $this->assertGreaterThanOrEqual(time(), $token->getExpires());
88 | $this->assertEquals('REFRESH_TOKEN', $token->getRefreshToken());
89 | $this->assertEquals('OPENID', $token->getValues()['openid']);
90 | $this->assertEquals('SCOPE', $token->getValues()['scope']);
91 | $this->assertEquals('o6_bmasdasdsad6_2sgVt7hMZOPfL', $token->getValues()['unionid']);
92 | $this->assertNull($token->getResourceOwnerId());
93 | }
94 |
95 | public function testUserData()
96 | {
97 | $openid = uniqid();
98 | $nickname = uniqid();
99 | $sex = random_int(1, 2);
100 | $province = uniqid();
101 | $city = uniqid();
102 | $country = uniqid();
103 | $headImagurl = uniqid();
104 | $privilege = '['.uniqid().','.uniqid().']';
105 | $unionid = uniqid();
106 |
107 | $postResponse = m::mock('Psr\Http\Message\ResponseInterface');
108 | $postResponse->shouldReceive('getBody')->andReturn('{"access_token": "ACCESS_TOKEN","expires_in": 7200,"refresh_token": "REFRESH_TOKEN","openid": "OPENID","scope": "SCOPE","unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"}');
109 | $postResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'json']);
110 |
111 |
112 | $userResponse = m::mock('Psr\Http\Message\ResponseInterface');
113 | $userResponse->shouldReceive('getBody')->andReturn('{"openid": "'.$openid.'","nickname": "'.$nickname.'","sex": "'.$sex.'","province": "'.$province.'","city": "'.$city.'","country": "'.$country.'","headimgurl": "'.$headImagurl.'","privilege": "'.$privilege.'","unionid": "'.$unionid.'"}');
114 | $userResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'json']);
115 |
116 |
117 | $client = m::mock('GuzzleHttp\ClientInterface');
118 | $client->shouldReceive('send')
119 | ->times(2)
120 | ->andReturn($postResponse, $userResponse);
121 | $this->provider->setHttpClient($client);
122 |
123 | $token = $this->provider->getAccessToken('authorization_code', ['code' => 'mock_authorization_code']);
124 |
125 | /** @var WebResourceOwner $user */
126 | $user = $this->provider->getResourceOwner($token);
127 |
128 | $this->assertEquals($openid, $user->getId());
129 | $this->assertEquals($nickname, $user->getNickname());
130 | $this->assertEquals($sex, $user->getSex());
131 | $this->assertEquals($province, $user->getProvince());
132 | $this->assertEquals($city, $user->getCity());
133 | $this->assertEquals($country, $user->getCountry());
134 | $this->assertEquals($headImagurl, $user->getHeadImgUrl());
135 | $this->assertEquals($privilege, $user->getPrivilege());
136 | $this->assertEquals($unionid, $user->getUnionId());
137 | }
138 |
139 | public function testUserDataFails()
140 | {
141 | $errorPayloads = [
142 | '{"errcode":40003,"errmsg": "invalid openid"}',
143 | '{"foo":"bar"}'
144 | ];
145 |
146 | $testPayload = function ($payload) {
147 | $postResponse = m::mock('Psr\Http\Message\ResponseInterface');
148 | $postResponse->shouldReceive('getBody')->andReturn('{"access_token": "ACCESS_TOKEN","expires_in": 7200,"refresh_token": "REFRESH_TOKEN","openid": "OPENID","scope": "SCOPE","unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"}');
149 | $postResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'json']);
150 |
151 | $userResponse = m::mock('Psr\Http\Message\ResponseInterface');
152 | $userResponse->shouldReceive('getBody')->andReturn($payload);
153 | $userResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'json']);
154 | $userResponse->shouldReceive('getStatusCode')->andReturn(500);
155 |
156 | $client = m::mock('GuzzleHttp\ClientInterface');
157 | $client->shouldReceive('send')
158 | ->times(2)
159 | ->andReturn($postResponse, $userResponse);
160 | $this->provider->setHttpClient($client);
161 |
162 | $token = $this->provider->getAccessToken('authorization_code', ['code' => 'mock_authorization_code']);
163 |
164 | try {
165 | $user = $this->provider->getResourceOwner($token);
166 | return false;
167 | } catch (\Exception $e) {
168 | $this->assertInstanceOf('\League\OAuth2\Client\Provider\Exception\IdentityProviderException', $e);
169 | }
170 |
171 | return $payload;
172 | };
173 |
174 | $this->assertCount(1, array_filter(array_map($testPayload, $errorPayloads)));
175 | }
176 |
177 | }
--------------------------------------------------------------------------------
/tests/src/Sample/.gitignore:
--------------------------------------------------------------------------------
1 | config.ini
--------------------------------------------------------------------------------
/tests/src/Sample/config.ini.dist:
--------------------------------------------------------------------------------
1 | appid = xxxxxxxxxxx
2 | secret = xxxxxxxxxx
3 | redirect_uri = http://example.com/your-redirect-url/
--------------------------------------------------------------------------------
/tests/src/Sample/web-sample.php:
--------------------------------------------------------------------------------
1 | $appid,
14 | 'secret' => $appsecret,
15 | 'redirect_uri' => $redirect_uri
16 | ]
17 | );
18 |
19 | // If we don't have an authorization code then get one
20 | if (!isset($_GET['code'])) {
21 |
22 |
23 | // Fetch the authorization URL from the provider; this returns the
24 | // urlAuthorize option and generates and applies any necessary parameters
25 | // (e.g. state).
26 | $authorizationUrl = $provider->getAuthorizationUrl();
27 |
28 | // Get the state generated for you and store it to the session.
29 | $_SESSION['oauth2state'] = $provider->getState();
30 |
31 | // Redirect the user to the authorization URL.
32 | header('Location: '.$authorizationUrl);
33 | exit;
34 |
35 | // Check given state against previously stored one to mitigate CSRF attack
36 | } elseif (empty($_GET['state']) || ($_GET['state'] !== rtrim($_SESSION['oauth2state'], '#wechat_redirect'))) {
37 |
38 | unset($_SESSION['oauth2state']);
39 | exit('Invalid state');
40 |
41 | } else {
42 |
43 | try {
44 |
45 | // Try to get an access token using the authorization code grant.
46 | $accessToken = $provider->getAccessToken(
47 | 'authorization_code',
48 | [
49 | 'code' => $_GET['code'],
50 | ]
51 | );
52 |
53 | // We have an access token, which we may use in authenticated
54 | // requests against the service provider's API.
55 | echo "token: ".$accessToken->getToken()."
";
56 | echo "refreshToken: ".$accessToken->getRefreshToken()."
";
57 | echo "Expires: ".$accessToken->getExpires()."
";
58 | echo ($accessToken->hasExpired() ? 'expired' : 'not expired')."
";
59 |
60 | // Using the access token, we may look up details about the
61 | // resource owner.
62 | $resourceOwner = $provider->getResourceOwner($accessToken);
63 |
64 | var_export($resourceOwner->toArray());
65 |
66 | // The provider provides a way to get an authenticated API request for
67 | // the service, using the access token; it returns an object conforming
68 | // to Psr\Http\Message\RequestInterface.
69 | // -----------
70 | // $request = $provider->getAuthenticatedRequest(
71 | // 'GET',
72 | // 'https://api.weixin.qq.com/sns/oauth2/refresh_token',
73 | // $accessToken,
74 | // ['scope' => 'snsapi_base']
75 | // );
76 | // $client = new \GuzzleHttp\Client();
77 | // $res = $client->send($request);
78 | // echo "
";
79 | // var_export($res->getBody());
80 |
81 |
82 | } catch (\League\OAuth2\Client\Provider\Exception\IdentityProviderException $e) {
83 |
84 | // Failed to get the access token or user details.
85 | echo "error:";
86 | exit($e->getMessage());
87 |
88 | }
89 |
90 |
91 | }
--------------------------------------------------------------------------------
/tests/src/Support/MiniProgram/MiniProgramDataCryptTest.php:
--------------------------------------------------------------------------------
1 | appid = 'wx4f4bc4dec97d474b';
19 | $this->sessionKey = 'tiihtNczf5v6AKRyjwEUhQ==';
20 | $this->encryptedData = "CiyLU1Aw2KjvrjMdj8YKliAjtP4gsMZM
21 | QmRzooG2xrDcvSnxIMXFufNstNGTyaGS
22 | 9uT5geRa0W4oTOb1WT7fJlAC+oNPdbB+
23 | 3hVbJSRgv+4lGOETKUQz6OYStslQ142d
24 | NCuabNPGBzlooOmB231qMM85d2/fV6Ch
25 | evvXvQP8Hkue1poOFtnEtpyxVLW1zAo6
26 | /1Xx1COxFvrc2d7UL/lmHInNlxuacJXw
27 | u0fjpXfz/YqYzBIBzD6WUfTIF9GRHpOn
28 | /Hz7saL8xz+W//FRAUid1OksQaQx4CMs
29 | 8LOddcQhULW4ucetDf96JcR3g0gfRK4P
30 | C7E/r7Z6xNrXd2UIeorGj5Ef7b1pJAYB
31 | 6Y5anaHqZ9J6nKEBvB4DnNLIVWSgARns
32 | /8wR2SiRS7MNACwTyrGvt9ts8p12PKFd
33 | lqYTopNHR1Vf7XjfhQlVsAJdNiKdYmYV
34 | oKlaRv85IfVunYzO0IKXsyl7JCUjCpoG
35 | 20f0a04COwfneQAGGwd5oa+T8yO5hzuy
36 | Db/XcxxmK01EpqOyuxINew==";
37 | $this->iv = 'r7BXXKkLb8qrSNn05n0qiA==';
38 | }
39 |
40 | public function tearDown()
41 | {
42 | m::close();
43 | parent::tearDown();
44 | }
45 |
46 | public function testDecryptData()
47 | {
48 | $dataCrypt = new MiniProgramDataCrypt($this->appid, $this->sessionKey);
49 | $errCode = $dataCrypt->decryptData($this->encryptedData, $this->iv, $data);
50 |
51 | $this->assertEquals(0, $errCode);
52 | $this->assertEquals('oGZUI0egBJY1zhBYw2KhdUfwVJJE', $data->openId);
53 | $this->assertEquals('Band', $data->nickName);
54 | $this->assertEquals(1, $data->gender);
55 | $this->assertEquals('zh_CN', $data->language);
56 | $this->assertEquals('Guangzhou', $data->city);
57 | $this->assertEquals('Guangdong', $data->province);
58 | $this->assertEquals('CN', $data->country);
59 | $this->assertEquals('http://wx.qlogo.cn/mmopen/vi_32/aSKcBBPpibyKNicHNTMM0qJVh8Kjgiak2AHWr8MHM4WgMEm7GFhsf8OYrySdbvAMvTsw3mo8ibKicsnfN5pRjl1p8HQ/0', $data->avatarUrl);
60 | $this->assertEquals('ocMvos6NjeKLIBqg5Mr9QjxrP1FA', $data->unionId);
61 | $this->assertEquals('1477314187', $data->watermark->timestamp);
62 | $this->assertEquals('wx4f4bc4dec97d474b', $data->watermark->appid);
63 | }
64 | }
--------------------------------------------------------------------------------