getAccessTokenByRequestParameters()   B
last analyzed

Complexity

Conditions 9
Paths 12

Size

Total Lines 30
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 52.4465

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 9
eloc 15
nc 12
nop 1
dl 0
loc 30
ccs 3
cts 16
cp 0.1875
crap 52.4465
rs 8.0555
c 2
b 0
f 0
1
<?php
2
/**
3
 * SocialConnect project
4
 * @author: Patsura Dmitry https://github.com/ovr <[email protected]>
5
 */
6
declare(strict_types=1);
7
8
namespace SocialConnect\OAuth2;
9
10
use Psr\Http\Message\RequestInterface;
11
use SocialConnect\OAuth2\Exception\InvalidState;
12
use SocialConnect\OAuth2\Exception\Unauthorized;
13
use SocialConnect\OAuth2\Exception\UnknownAuthorization;
14
use SocialConnect\OAuth2\Exception\UnknownState;
15
use SocialConnect\Provider\AbstractBaseProvider;
16
use SocialConnect\Provider\Exception\InvalidAccessToken;
17
use SocialConnect\Provider\Exception\InvalidResponse;
18
19
abstract class AbstractProvider extends AbstractBaseProvider
20
{
21
    /**
22
     * HTTP method for access token request
23
     *
24
     * @var string
25
     */
26
    protected $requestHttpMethod = 'POST';
27
28
    protected bool $pkce = false;
29
30
    /**
31
     * @return string
32
     */
33
    abstract public function getAuthorizeUri();
34
35
    /**
36
     * @return string
37
     */
38
    abstract public function getRequestTokenUri();
39
40
    /**
41
     * {@inheritdoc}
42
     */
43 23
    public function getAuthUrlParameters(): array
44
    {
45 23
        $parameters = parent::getAuthUrlParameters();
46
47
        // special parameters only required for OAuth2
48 23
        $parameters['client_id'] = $this->consumer->getKey();
49 23
        $parameters['redirect_uri'] = $this->getRedirectUrl();
50 23
        $parameters['response_type'] = 'code';
51
52 23
        if ($this->pkce) {
53 1
            $codeVerifier = $this->generatePKCECodeVerifier();
54 1
            $this->session->set('code_verifier', $codeVerifier);
55
56 1
            $parameters['code_challenge'] = $this->generatePKCECodeChallenge($codeVerifier);
57 1
            $parameters['code_challenge_method'] = 'S256';
58
        }
59
60 23
        return $parameters;
61
    }
62
63 1
    private function generatePKCECodeVerifier(int $length = 128)
64
    {
65 1
        if ($length < 43 || $length > 128) {
66
            throw new \Exception("Length must be between 43 and 128");
67
        }
68
69 1
        $randomBytes = random_bytes($length);
70 1
        return rtrim(strtr(base64_encode($randomBytes), '+/', '-_'), '=');
71
    }
72
73 1
    private function generatePKCECodeChallenge(string $codeVerifier)
74
    {
75 1
        $hash = hash('sha256', $codeVerifier, true);
76 1
        return rtrim(strtr(base64_encode($hash), '+/', '-_'), '=');
77
    }
78
79
    /**
80
     * {@inheritdoc}
81
     */
82 23
    public function makeAuthUrl(): string
83
    {
84 23
        $urlParameters = $this->getAuthUrlParameters();
85
86 23
        if (!$this->getBoolOption('stateless', false)) {
87 23
            $this->session->set(
88
                'oauth2_state',
89 23
                $urlParameters['state'] = $this->generateState(),
90
            );
91
        }
92
93 23
        if (count($this->scope) > 0) {
94 23
            $urlParameters['scope'] = $this->getScopeInline();
95
        }
96
97 23
        return $this->getAuthorizeUri() . '?' . http_build_query($urlParameters);
98
    }
99
100
    /**
101
     * Parse access token from response's $body
102
     *
103
     * @param string $body
104
     * @return AccessToken
105
     * @throws InvalidAccessToken
106
     */
107 82
    public function parseToken(string $body)
108
    {
109 82
        if (empty($body)) {
110 20
            throw new InvalidAccessToken('Provider response with empty body');
111
        }
112
113 62
        $token = json_decode($body, true);
114 62
        if ($token) {
115 21
            if (!is_array($token)) {
116
                throw new InvalidAccessToken('Response must be array');
117
            }
118
119 21
            return new AccessToken($token);
120
        }
121
122 41
        throw new InvalidAccessToken('Server response with not valid/empty JSON');
123
    }
124
125
    /**
126
     * @param string $code
127
     * @return RequestInterface
128
     */
129 22
    protected function makeAccessTokenRequest(string $code): RequestInterface
130
    {
131
        $parameters = [
132 22
            'client_id' => $this->consumer->getKey(),
133 22
            'client_secret' => $this->consumer->getSecret(),
134
            'code' => $code,
135
            'grant_type' => 'authorization_code',
136 22
            'redirect_uri' => $this->getRedirectUrl()
137
        ];
138
139 22
        if ($this->pkce) {
140 1
            $codeVerifier = $this->session->get('code_verifier');
141 1
            if (!$codeVerifier) {
142
                throw new \RuntimeException('PKCE code verifier not found in session');
143
            }
144
145 1
            $parameters['code_verifier'] = $codeVerifier;
146 1
            $parameters['device_id'] = $this->session->get('device_id');
147
148 1
            $this->session->delete('code_verifier');
149
        }
150
151 22
        return $this->httpStack->createRequest($this->requestHttpMethod, $this->getRequestTokenUri())
152 22
            ->withHeader('Content-Type', 'application/x-www-form-urlencoded')
153 22
            ->withBody($this->httpStack->createStream(http_build_query($parameters, '', '&')))
154
        ;
155
    }
156
157
    /**
158
     * @param string $code
159
     * @return AccessToken
160
     * @throws InvalidAccessToken
161
     * @throws InvalidResponse
162
     * @throws \Psr\Http\Client\ClientExceptionInterface
163
     */
164 24
    public function getAccessToken(string $code): AccessToken
165
    {
166 24
        $response = $this->executeRequest(
167 24
            $this->makeAccessTokenRequest($code)
168
        );
169
170
        return $this->parseToken($response->getBody()->getContents());
171
    }
172
173
    /**
174
     * @param array $parameters
175
     * @return AccessToken
176
     * @throws InvalidAccessToken
177
     * @throws InvalidResponse
178
     * @throws InvalidState
179
     * @throws Unauthorized
180
     * @throws UnknownAuthorization
181
     * @throws UnknownState
182
     * @throws \Psr\Http\Client\ClientExceptionInterface
183
     */
184 23
    public function getAccessTokenByRequestParameters(array $parameters)
185
    {
186 23
        if (isset($parameters['error']) && $parameters['error'] === 'access_denied') {
187 23
            throw new Unauthorized();
188
        }
189
190
        if (!isset($parameters['code'])) {
191
            throw new Unauthorized('Unknown code');
192
        }
193
194
        if (isset($parameters['device_id'])) {
195
            $this->session->set('device_id', $parameters['device_id']);
196
        }
197
198
        if (!$this->getBoolOption('stateless', false)) {
199
            $state = $this->session->get('oauth2_state');
200
            if (!$state) {
201
                throw new UnknownAuthorization();
202
            }
203
204
            if (!isset($parameters['state'])) {
205
                throw new UnknownState();
206
            }
207
208
            if ($state !== $parameters['state']) {
209
                throw new InvalidState();
210
            }
211
        }
212
213
        return $this->getAccessToken($parameters['code']);
214
    }
215
216
    /**
217
     * {@inheritDoc}
218
     */
219
    public function createAccessToken(array $information)
220
    {
221
        return new AccessToken($information);
222
    }
223
}
224