Passed
Pull Request — master (#193)
by
unknown
17:47 queued 02:51
created

AbstractProvider::generatePKCECodeVerifier()   A

Complexity

Conditions 3
Paths 2

Size

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