Completed
Push — master ( 4778d8...c5c7c0 )
by Дмитрий
01:42
created

src/OAuth2/AbstractProvider.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * SocialConnect project
4
 * @author: Patsura Dmitry https://github.com/ovr <[email protected]>
5
 */
6
7
namespace SocialConnect\OAuth2;
8
9
use InvalidArgumentException;
10
use SocialConnect\OAuth2\Exception\InvalidState;
11
use SocialConnect\OAuth2\Exception\Unauthorized;
12
use SocialConnect\OAuth2\Exception\UnknownAuthorization;
13
use SocialConnect\OAuth2\Exception\UnknownState;
14
use SocialConnect\Provider\AbstractBaseProvider;
15
use SocialConnect\Provider\Exception\InvalidAccessToken;
16
use SocialConnect\Provider\Exception\InvalidResponse;
17
use SocialConnect\Common\Http\Client\Client;
18
19
abstract class AbstractProvider extends AbstractBaseProvider
20
{
21
    /**
22
     * HTTP method for access token request
23
     *
24
     * @var string
25
     */
26
    protected $requestHttpMethod = Client::POST;
27
28
    /**
29
     * @return string
30
     */
31
    abstract public function getAuthorizeUri();
32
33
    /**
34
     * @return string
35
     */
36
    abstract public function getRequestTokenUri();
37
38
    /**
39
     * Default parameters for auth url, can be redeclared inside implementation of the Provider
40
     *
41
     * @return array
42
     */
43 18
    public function getAuthUrlParameters()
44
    {
45
        return [
46 18
            'client_id' => $this->consumer->getKey(),
47 18
            'redirect_uri' => $this->getRedirectUrl(),
48 18
            'response_type' => 'code',
49 18
        ];
50
    }
51
52
    /**
53
     * 16 bytes / 128 bit / 16 symbols / 32 symbols in hex
54
     */
55
    const STATE_BYTES = 16;
56
57
    /**
58
     * @return string
59
     */
60 36
    protected function generateState()
61
    {
62 36
        if (function_exists('random_bytes')) {
63
            return bin2hex(random_bytes(self::STATE_BYTES));
64
        }
65
66 36
        if (function_exists('openssl_random_pseudo_bytes')) {
67 36
            $bytes = openssl_random_pseudo_bytes(self::STATE_BYTES, $strongSource);
68 36
            if (!$strongSource) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $strongSource of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
69
                trigger_error(
70
                    'openssl was unable to use a strong source of entropy. ' .
71
                    'Consider updating your system libraries, or ensuring ' .
72
                    'you have more available entropy.',
73
                    E_USER_WARNING
74
                );
75
            }
76
77 36
            return bin2hex($bytes);
78
        }
79
80
        trigger_error(
81
            'You do not have a safe source of random data available. ' .
82
            'Install either the openssl extension, or paragonie/random_compat. ' .
83
            'Falling back to an insecure random source.',
84
            E_USER_WARNING
85
        );
86
87
        return md5(
88
            time() . mt_rand()
89
        );
90
    }
91
92
    /**
93
     * @return string
94
     */
95 18
    public function makeAuthUrl()
96
    {
97 18
        $urlParameters = $this->getAuthUrlParameters();
98
99 18
        $this->session->set(
100 18
            'oauth2_state',
101 18
            $urlParameters['state'] = $this->generateState()
102 18
        );
103
104 18
        if (count($this->scope) > 0) {
105
            $urlParameters['scope'] = $this->getScopeInline();
106
        }
107
108 18
        if (count($this->fields) > 0) {
109
            $urlParameters['fields'] = $this->getFieldsInline();
110
        }
111
112 18
        return $this->getAuthorizeUri() . '?' . http_build_query($urlParameters);
113
    }
114
115
    /**
116
     * Parse access token from response's $body
117
     *
118
     * @param string|bool $body
119
     * @return AccessToken
120
     * @throws InvalidAccessToken
121
     */
122 3
    public function parseToken($body)
123
    {
124 3
        if (empty($body)) {
125
            throw new InvalidAccessToken('Provider response with empty body');
126
        }
127
128 3
        parse_str($body, $token);
129
130 3
        if (!is_array($token) || !isset($token['access_token'])) {
131 2
            throw new InvalidAccessToken('Provider API returned an unexpected response');
132
        }
133
134 1
        return new AccessToken($token);
135
    }
136
137
    /**
138
     * @param string $code
139
     * @return \SocialConnect\Common\Http\Request
140
     */
141 19
    protected function makeAccessTokenRequest($code)
142
    {
143
        $parameters = [
144 19
            'client_id' => $this->consumer->getKey(),
145 19
            'client_secret' => $this->consumer->getSecret(),
146 19
            'code' => $code,
147 19
            'grant_type' => 'authorization_code',
148 19
            'redirect_uri' => $this->getRedirectUrl()
149 19
        ];
150
151 19
        return new \SocialConnect\Common\Http\Request(
152 19
            $this->getRequestTokenUri(),
153 19
            $parameters,
154 19
            $this->requestHttpMethod,
155
            [
156
                'Content-Type' => 'application/x-www-form-urlencoded'
157 19
            ]
158 19
        );
159
    }
160
161
    /**
162
     * @param string $code
163
     * @return AccessToken
164
     * @throws InvalidResponse
165
     */
166 36
    public function getAccessToken($code)
167
    {
168 36
        if (!is_string($code)) {
169 18
            throw new InvalidArgumentException('Parameter $code must be a string');
170
        }
171
172 18
        $response = $this->httpClient->fromRequest(
173 18
            $this->makeAccessTokenRequest($code)
174 18
        );
175
176 18
        if (!$response->isSuccess()) {
177 18
            throw new InvalidResponse(
178 18
                'API response with error code',
179
                $response
180 18
            );
181
        }
182
183
        $body = $response->getBody();
184
        return $this->parseToken($body);
185
    }
186
187
    /**
188
     * @param array $parameters
189
     * @return AccessToken
190
     * @throws \SocialConnect\OAuth2\Exception\InvalidState
191
     * @throws \SocialConnect\OAuth2\Exception\UnknownState
192
     * @throws \SocialConnect\OAuth2\Exception\UnknownAuthorization
193
     */
194 18
    public function getAccessTokenByRequestParameters(array $parameters)
195
    {
196 18
        $state = $this->session->get('oauth2_state');
197 18
        if (!$state) {
198
            throw new UnknownAuthorization();
199
        }
200
201 18
        if (isset($parameters['error']) && $parameters['error'] === 'access_denied') {
202 18
            throw new Unauthorized();
203
        }
204
205
        if (!isset($parameters['state'])) {
206
            throw new UnknownState();
207
        }
208
209
        if ($state !== $parameters['state']) {
210
            throw new InvalidState();
211
        }
212
213
        if (!isset($parameters['code'])) {
214
            throw new Unauthorized('Unknown code');
215
        }
216
217
        return $this->getAccessToken($parameters['code']);
218
    }
219
}
220