• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

jwilsson / php-oauth2-client / 29638069374

18 Jul 2026 08:48AM UTC coverage: 95.506% (-4.5%) from 100.0%
29638069374

push

github

jwilsson
Disallow state length values less than 1

3 of 3 new or added lines in 1 file covered. (100.0%)

8 existing lines in 1 file now uncovered.

170 of 178 relevant lines covered (95.51%)

9.55 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.45
/src/Grant.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace OAuth2;
6

7
use OAuth2\Grant\Exception\GrantException;
8
use OAuth2\Token;
9
use OAuth2\Util\QueryTrait;
10
use Psr\Http\Client\ClientInterface;
11
use Psr\Http\Message\RequestFactoryInterface;
12
use Psr\Http\Message\RequestInterface;
13
use Psr\Http\Message\ResponseInterface;
14
use Psr\Http\Message\StreamFactoryInterface;
15

16
abstract class Grant
17
{
18
    use QueryTrait;
19

20
    /**
21
     *  @var array<string, mixed> Options for the provider to use
22
     */
23
    protected array $options;
24

25
    /**
26
     * @var ClientInterface A PSR-18 compatible HTTP client to use.
27
     */
28
    protected ClientInterface $httpClient;
29

30
    /**
31
     * @var RequestFactoryInterface A PSR-17 compatible request factory to use.
32
     */
33
    protected RequestFactoryInterface $requestFactory;
34

35
    /**
36
     * @var StreamFactoryInterface A PSR-17 compatible stream factory to use.
37
     */
38
    protected StreamFactoryInterface $streamFactory;
39

40
    /**
41
     * Constructor, set options and instantiate common classes.
42
     *
43
     * @param array<string, mixed> $options Options for the provider to use.
44
     * @param ClientInterface $httpClient A PSR-18 compatible HTTP client to use.
45
     * @param RequestFactoryInterface $requestFactory A PSR-17 compatible request factory to use.
46
     * @param StreamFactoryInterface $streamFactory A PSR-17 compatible stream factory to use.
47
     */
48
    public function __construct(
49
        array $options,
50
        ClientInterface $httpClient,
51
        RequestFactoryInterface $requestFactory,
52
        StreamFactoryInterface $streamFactory,
53
    ) {
54
        $this->options = $options;
44✔
55
        $this->httpClient = $httpClient;
44✔
56
        $this->requestFactory = $requestFactory;
44✔
57
        $this->streamFactory = $streamFactory;
44✔
58
    }
59

60
    /**
61
     * Create an authorization URL.
62
     *
63
     * @param string $state A random, secret value used to protect aginst CSRF attacks.
64
     * @param array<string, mixed> $parameters Parameters to include in the authorization URL.
65
     *
66
     * @return string
67
     */
68
    protected function createAuthorizationUrl(string $state, array $parameters = []): string
69
    {
70
        $parameters = array_replace([
4✔
71
            'client_id' => $this->options['client_id'],
4✔
72
            'redirect_uri' => $this->options['redirect_uri'] ?? null,
4✔
73
            'response_type' => 'code',
4✔
74
            'scope' => null,
4✔
75
            'state' => $state,
4✔
76
        ], $parameters);
4✔
77

78
        $url = $this->options['endpoints']['auth_url'];
4✔
79
        $sep = str_contains($url, '?') ? '&' : '?';
4✔
80

81
        return $url . $sep . $this->buildQuery($parameters);
4✔
82
    }
83

84
    /**
85
     * Create a new token request instance.
86
     *
87
     * @param array<string, mixed> $parameters Parameters to pass to the authorization server.
88
     *
89
     * @return RequestInterface
90
     */
91
    protected function createTokenRequest(array $parameters): RequestInterface
92
    {
93
        $body = $this->buildQuery($parameters);
30✔
94
        $body = $this->streamFactory->createStream($body);
30✔
95

96
        $request = $this->requestFactory->createRequest('POST', $this->options['endpoints']['token_url']);
30✔
97
        $request = $request->withHeader('Content-Type', 'application/x-www-form-urlencoded');
30✔
98
        $request = $request->withHeader('Accept', 'application/json');
30✔
99
        $request = $request->withBody($body);
30✔
100

101
        return $request;
30✔
102
    }
103

104
    /**
105
     * Generate a random state value.
106
     *
107
     * @param int $length Length of the state value.
108
     *
109
     * @throws \UnexpectedValueException
110
     *
111
     * @return string
112
     */
113
    public function generateState(int $length = 32): string
114
    {
115
        if ($length < 1) {
10✔
116
            throw new \UnexpectedValueException('State length must be greater than zero.');
2✔
117
        }
118

119
        // Length will be doubled when converting to hex
120
        $state = bin2hex(
8✔
121
            random_bytes(max(1, (int) ceil($length / 2))),
8✔
122
        );
8✔
123

124
        return substr($state, 0, $length);
8✔
125
    }
126

127
    /**
128
     * Extract an error message from the response body.
129
     *
130
     * @param array<string, mixed> $body
131
     * @param string $fallback
132
     *
133
     * @return string
134
     */
135
    protected function getErrorMessage(array $body, string $fallback): string
136
    {
137
        $message = $body['error_description'] ?? $body['error'] ?? null;
10✔
138

139
        return is_string($message) ? $message : $fallback;
10✔
140
    }
141

142
    /**
143
     * Send a previously instantiated token request.
144
     *
145
     * @param RequestInterface $request The token request object.
146
     *
147
     * @throws GrantException
148
     *
149
     * @return Token
150
     */
151
    protected function sendTokenRequest(RequestInterface $request): Token
152
    {
153
        $response = $this->httpClient->sendRequest($request);
30✔
154
        $body = json_decode($response->getBody()->__toString(), true);
30✔
155

156
        if (!is_array($body)) {
30✔
UNCOV
157
            throw new GrantException('Invalid JSON response.', $response->getStatusCode(), $response);
×
158
        }
159

160
        if ($response->getStatusCode() !== 200) {
30✔
161
            throw new GrantException(
10✔
162
                $this->getErrorMessage($body, 'Token request failed.'),
10✔
163
                $response->getStatusCode(),
10✔
164
                $response,
10✔
165
            );
10✔
166
        }
167

168
        if (isset($body['error'])) {
20✔
UNCOV
169
            throw new GrantException(
×
UNCOV
170
                $this->getErrorMessage($body, 'Token request failed.'),
×
UNCOV
171
                $response->getStatusCode(),
×
UNCOV
172
                $response,
×
UNCOV
173
            );
×
174
        }
175

176
        $this->validateTokenResponse($body, $response);
20✔
177

178
        return new Token($body);
10✔
179
    }
180

181
    /**
182
     * Validate the response body for a token request.
183
     *
184
     * @param array<string, mixed> $body
185
     *
186
     * @throws GrantException
187
     */
188
    protected function validateTokenResponse(array $body, ResponseInterface $response): void
189
    {
190
        if (!isset($body['access_token']) || !is_string($body['access_token']) || $body['access_token'] === '') {
20✔
191
            throw new GrantException('No valid access token present in response.', $response->getStatusCode(), $response);
10✔
192
        }
193

194
        foreach (['refresh_token', 'scope', 'token_type'] as $field) {
10✔
195
            if (array_key_exists($field, $body) && !is_string($body[$field])) {
10✔
UNCOV
196
                throw new GrantException(sprintf('Invalid %s value in response.', $field), $response->getStatusCode(), $response);
×
197
            }
198
        }
199

200
        foreach (['expires', 'expires_in'] as $field) {
10✔
201
            if (array_key_exists($field, $body) && !is_int($body[$field])) {
10✔
UNCOV
202
                throw new GrantException(sprintf('Invalid %s value in response.', $field), $response->getStatusCode(), $response);
×
203
            }
204
        }
205
    }
206
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc