Passed
Pull Request — master (#193)
by
unknown
14:49
created

AbstractProvider::generatePKCECodeVerifier()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 3

Importance

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