├── .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 | [![Latest Version](https://img.shields.io/github/release/oakhope/oauth2-wechat.svg?style=flat-square)](https://github.com/oakhope/oauth2-wechat/releases) 4 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) 5 | [![Build Status](https://img.shields.io/travis/oakhope/oauth2-wechat/master.svg?style=flat-square)](https://travis-ci.org/oakhope/oauth2-wechat) 6 | [![Coverage Status](https://img.shields.io/scrutinizer/coverage/g/oakhope/oauth2-wechat.svg?style=flat-square)](https://scrutinizer-ci.com/g/oakhope/oauth2-wechat/code-structure) 7 | [![Quality Score](https://img.shields.io/scrutinizer/g/oakhope/oauth2-wechat.svg?style=flat-square)](https://scrutinizer-ci.com/g/oakhope/oauth2-wechat) 8 | [![Total Downloads](https://img.shields.io/packagist/dt/oakhope/oauth2-wechat.svg?style=flat-square)](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 | } --------------------------------------------------------------------------------