AuthorizationService::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 5
nc 1
nop 5
dl 0
loc 12
ccs 6
cts 6
cp 1
crap 1
rs 10
c 1
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Facile\OpenIDClient\Service;
6
7
use function array_filter;
8
use function array_key_exists;
9
use function array_merge;
10
use Facile\OpenIDClient\Client\ClientInterface as OpenIDClient;
11
use Facile\OpenIDClient\Exception\InvalidArgumentException;
12
use Facile\OpenIDClient\Exception\OAuth2Exception;
13
use Facile\OpenIDClient\Exception\RuntimeException;
14
use function Facile\OpenIDClient\get_endpoint_uri;
15
use function Facile\OpenIDClient\parse_callback_params;
16
use function Facile\OpenIDClient\parse_metadata_response;
17
use Facile\OpenIDClient\Session\AuthSessionInterface;
18
use Facile\OpenIDClient\Token\IdTokenVerifierBuilderInterface;
19
use Facile\OpenIDClient\Token\TokenSetFactoryInterface;
20
use Facile\OpenIDClient\Token\TokenSetInterface;
21
use Facile\OpenIDClient\Token\TokenVerifierBuilderInterface;
22
use function http_build_query;
23
use function is_array;
24
use function is_string;
25
use function json_encode;
26
use JsonSerializable;
27
use Psr\Http\Client\ClientExceptionInterface;
28
use Psr\Http\Client\ClientInterface;
29
use Psr\Http\Message\RequestFactoryInterface;
30
use Psr\Http\Message\ServerRequestInterface;
31
32
/**
33
 * OAuth 2.0
34
 *
35
 * @link https://tools.ietf.org/html/rfc6749 RFC 6749
36
 *
37
 * @psalm-import-type TokenSetMixedType from TokenSetInterface
38
 */
39
final class AuthorizationService
40
{
41
    /** @var TokenSetFactoryInterface */
42
    private $tokenSetFactory;
43
44
    /** @var ClientInterface */
45
    private $client;
46
47
    /** @var RequestFactoryInterface */
48
    private $requestFactory;
49
50
    /** @var IdTokenVerifierBuilderInterface */
51
    private $idTokenVerifierBuilder;
52
53
    /** @var TokenVerifierBuilderInterface */
54
    private $responseVerifierBuilder;
55
56 2
    public function __construct(
57
        TokenSetFactoryInterface $tokenSetFactory,
58
        ClientInterface $client,
59
        RequestFactoryInterface $requestFactory,
60
        IdTokenVerifierBuilderInterface $idTokenVerifierBuilder,
61
        TokenVerifierBuilderInterface $responseVerifierBuilder
62
    ) {
63 2
        $this->tokenSetFactory = $tokenSetFactory;
64 2
        $this->client = $client;
65 2
        $this->requestFactory = $requestFactory;
66 2
        $this->idTokenVerifierBuilder = $idTokenVerifierBuilder;
67 2
        $this->responseVerifierBuilder = $responseVerifierBuilder;
68 2
    }
69
70
    /**
71
     * @param OpenIDClient $client
72
     * @param array<string, mixed> $params
73
     *
74
     * @return string
75
     *
76
     * @template P as (array{scope?: string, response_type?: string, redirect_uri?: string, claims?: array<string, string>|JsonSerializable}&array<string, mixed>)|array<empty, empty>
77
     * @psalm-param P $params
78
     */
79 1
    public function getAuthorizationUri(OpenIDClient $client, array $params = []): string
80
    {
81 1
        $clientMetadata = $client->getMetadata();
82 1
        $issuerMetadata = $client->getIssuer()->getMetadata();
83 1
        $endpointUri = $issuerMetadata->getAuthorizationEndpoint();
84
85 1
        $params = array_merge([
86 1
            'client_id' => $clientMetadata->getClientId(),
87 1
            'scope' => 'openid',
88 1
            'response_type' => $clientMetadata->getResponseTypes()[0] ?? 'code',
89 1
            'redirect_uri' => $clientMetadata->getRedirectUris()[0] ?? null,
90 1
        ], $params);
91
92 1
        $params = array_filter($params, static function ($value): bool {
93 1
            return null !== $value;
94 1
        });
95
96
        /**
97
         * @var string $key
98
         * @var mixed $value
99
         */
100 1
        foreach ($params as $key => $value) {
101 1
            if (null === $value) {
102
                unset($params[$key]);
103 1
            } elseif ('claims' === $key && (is_array($value) || $value instanceof JsonSerializable)) {
104
                $params['claims'] = json_encode($value);
105 1
            } elseif (! is_string($value)) {
106
                $params[$key] = (string) $value;
107
            }
108
        }
109
110 1
        if (! array_key_exists('nonce', $params) && 'code' !== ($params['response_type'] ?? '')) {
111
            throw new InvalidArgumentException('nonce MUST be provided for implicit and hybrid flows');
112
        }
113
114 1
        return $endpointUri . '?' . http_build_query($params);
115
    }
116
117
    /**
118
     * @param ServerRequestInterface $serverRequest
119
     * @param OpenIDClient $client
120
     *
121
     * @throws OAuth2Exception
122
     *
123
     * @return array<string, mixed>
124
     *
125
     * @psalm-return TokenSetMixedType
126
     */
127
    public function getCallbackParams(ServerRequestInterface $serverRequest, OpenIDClient $client): array
128
    {
129
        return $this->processResponseParams($client, parse_callback_params($serverRequest));
130
    }
131
132
    /**
133
     * @param OpenIDClient $client
134
     * @param array<string, mixed> $params
135
     * @param string|null $redirectUri
136
     * @param AuthSessionInterface|null $authSession
137
     * @param int|null $maxAge
138
     *
139
     * @return TokenSetInterface
140
     *
141
     * @psalm-param TokenSetMixedType $params
142
     */
143
    public function callback(
144
        OpenIDClient $client,
145
        array $params,
146
        ?string $redirectUri = null,
147
        ?AuthSessionInterface $authSession = null,
148
        ?int $maxAge = null
149
    ): TokenSetInterface {
150
        $tokenSet = $this->tokenSetFactory->fromArray($params);
151
152
        $idToken = $tokenSet->getIdToken();
153
154
        if (null !== $idToken) {
155
            $claims = $this->idTokenVerifierBuilder->build($client)
156
                ->withNonce(null !== $authSession ? $authSession->getNonce() : null)
157
                ->withState(null !== $authSession ? $authSession->getState() : null)
158
                ->withCode($tokenSet->getCode())
159
                ->withMaxAge($maxAge)
160
                ->withAccessToken($tokenSet->getAccessToken())
161
                ->verify($idToken);
162
            $tokenSet = $tokenSet->withClaims($claims);
163
        }
164
165
        if (null === $tokenSet->getCode()) {
166
            return $tokenSet;
167
        }
168
169
        // get token
170
        return $this->fetchToken($client, $tokenSet, $redirectUri, $authSession, $maxAge);
171
    }
172
173
    /**
174
     * @param OpenIDClient $client
175
     * @param TokenSetInterface $tokenSet
176
     * @param string|null $redirectUri
177
     * @param AuthSessionInterface|null $authSession
178
     * @param int|null $maxAge
179
     *
180
     * @throws OAuth2Exception
181
     *
182
     * @return TokenSetInterface
183
     */
184
    public function fetchToken(
185
        OpenIDClient $client,
186
        TokenSetInterface $tokenSet,
187
        ?string $redirectUri = null,
188
        ?AuthSessionInterface $authSession = null,
189
        ?int $maxAge = null
190
    ): TokenSetInterface {
191
        $code = $tokenSet->getCode();
192
193
        if (null === $code) {
194
            throw new RuntimeException('Unable to fetch token without a code');
195
        }
196
197
        if (null === $redirectUri) {
198
            $redirectUri = $client->getMetadata()->getRedirectUris()[0] ?? null;
199
        }
200
201
        if (null === $redirectUri) {
202
            throw new InvalidArgumentException('A redirect_uri should be provided');
203
        }
204
205
        $params = [
206
            'grant_type' => 'authorization_code',
207
            'code' => $code,
208
            'redirect_uri' => $redirectUri,
209
        ];
210
211
        if (null !== $authSession && null !== $authSession->getCodeVerifier()) {
212
            $params['code_verifier'] = $authSession->getCodeVerifier();
213
        }
214
215
        $tokenSet = $this->grant($client, $params);
216
217
        $idToken = $tokenSet->getIdToken();
218
219
        if (null !== $idToken) {
220
            $claims = $this->idTokenVerifierBuilder->build($client)
221
                ->withNonce(null !== $authSession ? $authSession->getNonce() : null)
222
                ->withState(null !== $authSession ? $authSession->getState() : null)
223
                ->withMaxAge($maxAge)
224
                ->verify($idToken);
225
            $tokenSet = $tokenSet->withClaims($claims);
226
        }
227
228
        return $tokenSet;
229
    }
230
231
    /**
232
     * @param OpenIDClient $client
233
     * @param string $refreshToken
234
     * @param array<string, mixed> $params
235
     *
236
     * @return TokenSetInterface
237
     */
238
    public function refresh(OpenIDClient $client, string $refreshToken, array $params = []): TokenSetInterface
239
    {
240
        $tokenSet = $this->grant($client, array_merge($params, [
241
            'grant_type' => 'refresh_token',
242
            'refresh_token' => $refreshToken,
243
        ]));
244
245
        $idToken = $tokenSet->getIdToken();
246
247
        if (null === $idToken) {
248
            return $tokenSet;
249
        }
250
251
        $idToken = $tokenSet->getIdToken();
252
253
        if (null !== $idToken) {
254
            $claims = $this->idTokenVerifierBuilder->build($client)
255
                ->withAccessToken($tokenSet->getAccessToken())
256
                ->verify($idToken);
257
            $tokenSet = $tokenSet->withClaims($claims);
258
        }
259
260
        return $tokenSet;
261
    }
262
263
    /**
264
     * @param OpenIDClient $client
265
     * @param array<string, mixed> $params
266
     *
267
     * @throws OAuth2Exception
268
     *
269
     * @return TokenSetInterface
270
     */
271 1
    public function grant(OpenIDClient $client, array $params = []): TokenSetInterface
272
    {
273 1
        $authMethod = $client->getAuthMethodFactory()
274 1
            ->create($client->getMetadata()->getTokenEndpointAuthMethod());
275
276 1
        $endpointUri = get_endpoint_uri($client, 'token_endpoint');
277
278 1
        $tokenRequest = $this->requestFactory->createRequest('POST', $endpointUri)
279 1
            ->withHeader('content-type', 'application/x-www-form-urlencoded');
280
281 1
        $tokenRequest = $authMethod->createRequest($tokenRequest, $client, $params);
282
283 1
        $httpClient = $client->getHttpClient() ?? $this->client;
284
285
        try {
286 1
            $response = $httpClient->sendRequest($tokenRequest);
287
        } catch (ClientExceptionInterface $e) {
288
            throw new RuntimeException('Unable to get token response', 0, $e);
289
        }
290
291
        /** @var TokenSetMixedType|array{error?: string}|array{response?: string} $data */
292 1
        $data = parse_metadata_response($response);
293 1
        $params = $this->processResponseParams($client, $data);
0 ignored issues
show
Bug introduced by
It seems like $data can also be of type Facile\OpenIDClient\Service\TokenSetMixedType; however, parameter $params of Facile\OpenIDClient\Serv...processResponseParams() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

293
        $params = $this->processResponseParams($client, /** @scrutinizer ignore-type */ $data);
Loading history...
294
295 1
        return $this->tokenSetFactory->fromArray($params);
296
    }
297
298
    /**
299
     * @param array<string, mixed> $params
300
     * @return bool
301
     *
302
     * @template P as array<string, mixed>
303
     * @psalm-param P $params
304
     * @psalm-assert-if-true array{response: string} $params
305
     */
306 1
    private function isResponseObject(array $params): bool
307
    {
308 1
        return array_key_exists('response', $params);
309
    }
310
311
    /**
312
     * @throws OAuth2Exception
313
     *
314
     * @template P as array<string, mixed>
315
     * @template OE as array{error: string, error_description?: string, error_uri?: string}
316
     * @psalm-param OE|P $params
317
     * @psalm-assert P $params
318
     */
319 1
    private function assertOAuth2Error(array $params): void
320
    {
321 1
        if (array_key_exists('error', $params)) {
322
            throw OAuth2Exception::fromParameters($params);
323
        }
324 1
    }
325
326
    /**
327
     * @param OpenIDClient $client
328
     * @param array<string, mixed> $params
329
     *
330
     * @throws OAuth2Exception
331
     *
332
     * @return array<string, mixed>
333
     *
334
     * @template R as array<string, mixed>
335
     * @template ResObject as array{response: string}
336
     * @psalm-param R $params
337
     * @psalm-return (R is ResObject ? TokenSetMixedType : R)
338
     */
339 1
    private function processResponseParams(OpenIDClient $client, array $params): array
340
    {
341 1
        $this->assertOAuth2Error($params);
342
343 1
        if ($this->isResponseObject($params)) {
344
            /** @var TokenSetMixedType|ResObject $params */
345
            $params = $this->responseVerifierBuilder->build($client)
346
                ->verify($params['response']);
347
348
            $this->assertOAuth2Error($params);
0 ignored issues
show
Bug introduced by
$params of type Facile\OpenIDClient\Serv...rvice\TokenSetMixedType is incompatible with the type array expected by parameter $params of Facile\OpenIDClient\Serv...ce::assertOAuth2Error(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

348
            $this->assertOAuth2Error(/** @scrutinizer ignore-type */ $params);
Loading history...
349
        }
350
351 1
        return $params;
352
    }
353
}
354