Completed
Push — master ( deca97...648369 )
by Thomas Mauro
01:44
created

AuthorizationService   A

Complexity

Total Complexity 34

Size/Duplication

Total Lines 255
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 23

Test Coverage

Coverage 39.05%

Importance

Changes 0
Metric Value
dl 0
loc 255
ccs 41
cts 105
cp 0.3905
rs 9.68
c 0
b 0
f 0
wmc 34
lcom 1
cbo 23

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 13 1
B getAuthorizationUri() 0 33 9
A getCallbackParams() 0 4 1
A callback() 0 29 5
B fetchToken() 0 46 9
A refresh() 0 24 3
A grant() 0 24 2
A processResponseParams() 0 17 4
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\IdTokenVerifierBuilder;
19
use Facile\OpenIDClient\Token\IdTokenVerifierBuilderInterface;
20
use Facile\OpenIDClient\Token\ResponseVerifierBuilder;
21
use Facile\OpenIDClient\Token\TokenSetFactory;
22
use Facile\OpenIDClient\Token\TokenSetFactoryInterface;
23
use Facile\OpenIDClient\Token\TokenSetInterface;
24
use Facile\OpenIDClient\Token\TokenVerifierBuilderInterface;
25
use Http\Discovery\Psr17FactoryDiscovery;
26
use Http\Discovery\Psr18ClientDiscovery;
27
use function http_build_query;
28
use function is_array;
29
use function is_string;
30
use function json_encode;
31
use JsonSerializable;
32
use Psr\Http\Client\ClientExceptionInterface;
33
use Psr\Http\Client\ClientInterface;
34
use Psr\Http\Message\RequestFactoryInterface;
35
use Psr\Http\Message\ServerRequestInterface;
36
37
/**
38
 * OAuth 2.0
39
 *
40
 * @link https://tools.ietf.org/html/rfc6749 RFC 6749
41
 */
42
class AuthorizationService
43
{
44
    /** @var TokenSetFactoryInterface */
45
    private $tokenSetFactory;
46
47
    /** @var ClientInterface */
48
    private $client;
49
50
    /** @var RequestFactoryInterface */
51
    private $requestFactory;
52
53
    /** @var IdTokenVerifierBuilderInterface */
54
    private $idTokenVerifierBuilder;
55
56
    /** @var TokenVerifierBuilderInterface */
57
    private $responseVerifiierBuilder;
58
59 2
    public function __construct(
60
        ?TokenSetFactoryInterface $tokenSetFactory = null,
61
        ?ClientInterface $client = null,
62
        ?RequestFactoryInterface $requestFactory = null,
63
        ?IdTokenVerifierBuilderInterface $idTokenVerifierBuilder = null,
64
        ?TokenVerifierBuilderInterface $responseVerifierBuilder = null
65
    ) {
66 2
        $this->tokenSetFactory = $tokenSetFactory ?? new TokenSetFactory();
67 2
        $this->client = $client ?? Psr18ClientDiscovery::find();
68 2
        $this->requestFactory = $requestFactory ?? Psr17FactoryDiscovery::findRequestFactory();
69 2
        $this->idTokenVerifierBuilder = $idTokenVerifierBuilder ?? new IdTokenVerifierBuilder();
70 2
        $this->responseVerifiierBuilder = $responseVerifierBuilder ?? new ResponseVerifierBuilder();
71 2
    }
72
73
    /**
74
     * @param OpenIDClient $client
75
     * @param array<string, mixed> $params
76
     *
77
     * @return string
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
        $params = array_filter($params, static function ($value): bool {
93 1
            return null !== $value;
94 1
        });
95
96 1
        foreach ($params as $key => $value) {
97 1
            if (null === $value) {
98
                unset($params[$key]);
99 1
            } elseif ('claims' === $key && (is_array($value) || $value instanceof JsonSerializable)) {
0 ignored issues
show
Bug introduced by
The class JsonSerializable does not exist. Is this class maybe located in a folder that is not analyzed, or in a newer version of your dependencies than listed in your composer.lock/composer.json?
Loading history...
100
                $params['claims'] = json_encode($value);
101 1
            } elseif (! is_string($value)) {
102
                $params[$key] = (string) $value;
103
            }
104
        }
105
106 1
        if (! array_key_exists('nonce', $params) && 'code' !== ($params['response_type'] ?? '')) {
107
            throw new InvalidArgumentException('nonce MUST be provided for implicit and hybrid flows');
108
        }
109
110 1
        return $endpointUri . '?' . http_build_query($params);
111
    }
112
113
    /**
114
     * @param ServerRequestInterface $serverRequest
115
     * @param OpenIDClient $client
116
     *
117
     * @return array<string, mixed>
0 ignored issues
show
Documentation introduced by
The doc-type array<string, could not be parsed: Expected ">" at position 5, but found "end of type". (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
118
     */
119
    public function getCallbackParams(ServerRequestInterface $serverRequest, OpenIDClient $client): array
120
    {
121
        return $this->processResponseParams($client, parse_callback_params($serverRequest));
122
    }
123
124
    /**
125
     * @param OpenIDClient $client
126
     * @param array<string, mixed> $params
127
     * @param string|null $redirectUri
128
     * @param AuthSessionInterface|null $authSession
129
     * @param int|null $maxAge
130
     *
131
     * @return TokenSetInterface
132
     */
133
    public function callback(
134
        OpenIDClient $client,
135
        array $params,
136
        ?string $redirectUri = null,
137
        ?AuthSessionInterface $authSession = null,
138
        ?int $maxAge = null
139
    ): TokenSetInterface {
140
        $tokenSet = $this->tokenSetFactory->fromArray($params);
141
142
        $idToken = $tokenSet->getIdToken();
143
144
        if (null !== $idToken) {
145
            $claims = $this->idTokenVerifierBuilder->build($client)
146
                ->withNonce(null !== $authSession ? $authSession->getNonce() : null)
147
                ->withState(null !== $authSession ? $authSession->getState() : null)
148
                ->withCode($tokenSet->getCode())
149
                ->withMaxAge($maxAge)
150
                ->withAccessToken($tokenSet->getAccessToken())
151
                ->verify($idToken);
152
            $tokenSet = $tokenSet->withClaims($claims);
153
        }
154
155
        if (null === $tokenSet->getCode()) {
156
            return $tokenSet;
157
        }
158
159
        // get token
160
        return $this->fetchToken($client, $tokenSet, $redirectUri, $authSession, $maxAge);
161
    }
162
163
    public function fetchToken(
164
        OpenIDClient $client,
165
        TokenSetInterface $tokenSet,
166
        ?string $redirectUri = null,
167
        ?AuthSessionInterface $authSession = null,
168
        ?int $maxAge = null
169
    ): TokenSetInterface {
170
        $code = $tokenSet->getCode();
171
172
        if (null === $code) {
173
            throw new RuntimeException('Unable to fetch token without a code');
174
        }
175
176
        if (null === $redirectUri) {
177
            $redirectUri = $client->getMetadata()->getRedirectUris()[0] ?? null;
178
        }
179
180
        if (null === $redirectUri) {
181
            throw new InvalidArgumentException('A redirect_uri should be provided');
182
        }
183
184
        $params = [
185
            'grant_type' => 'authorization_code',
186
            'code' => $code,
187
            'redirect_uri' => $redirectUri,
188
        ];
189
190
        if ($authSession && null !== $authSession->getCodeVerifier()) {
191
            $params['code_verifier'] = $authSession->getCodeVerifier();
192
        }
193
194
        $tokenSet = $this->grant($client, $params);
195
196
        $idToken = $tokenSet->getIdToken();
197
198
        if (null !== $idToken) {
199
            $claims = $this->idTokenVerifierBuilder->build($client)
200
                ->withNonce(null !== $authSession ? $authSession->getNonce() : null)
201
                ->withState(null !== $authSession ? $authSession->getState() : null)
202
                ->withMaxAge($maxAge)
203
                ->verify($idToken);
204
            $tokenSet = $tokenSet->withClaims($claims);
205
        }
206
207
        return $tokenSet;
208
    }
209
210
    /**
211
     * @param OpenIDClient $client
212
     * @param string $refreshToken
213
     * @param array<string, mixed> $params
214
     *
215
     * @return TokenSetInterface
216
     */
217
    public function refresh(OpenIDClient $client, string $refreshToken, array $params = []): TokenSetInterface
218
    {
219
        $tokenSet = $this->grant($client, array_merge($params, [
220
            'grant_type' => 'refresh_token',
221
            'refresh_token' => $refreshToken,
222
        ]));
223
224
        $idToken = $tokenSet->getIdToken();
225
226
        if (null === $idToken) {
227
            return $tokenSet;
228
        }
229
230
        $idToken = $tokenSet->getIdToken();
231
232
        if (null !== $idToken) {
233
            $claims = $this->idTokenVerifierBuilder->build($client)
234
                ->withAccessToken($tokenSet->getAccessToken())
235
                ->verify($idToken);
236
            $tokenSet = $tokenSet->withClaims($claims);
237
        }
238
239
        return $tokenSet;
240
    }
241
242
    /**
243
     * @param OpenIDClient $client
244
     * @param array<string, mixed> $params
245
     *
246
     * @return TokenSetInterface
247
     */
248 1
    public function grant(OpenIDClient $client, array $params = []): TokenSetInterface
249
    {
250 1
        $authMethod = $client->getAuthMethodFactory()
251 1
            ->create($client->getMetadata()->getTokenEndpointAuthMethod());
252
253 1
        $endpointUri = get_endpoint_uri($client, 'token_endpoint');
254
255 1
        $tokenRequest = $this->requestFactory->createRequest('POST', $endpointUri)
256 1
            ->withHeader('content-type', 'application/x-www-form-urlencoded');
257
258 1
        $tokenRequest = $authMethod->createRequest($tokenRequest, $client, $params);
259
260 1
        $httpClient = $client->getHttpClient() ?? $this->client;
261
262
        try {
263 1
            $response = $httpClient->sendRequest($tokenRequest);
264
        } catch (ClientExceptionInterface $e) {
265
            throw new RuntimeException('Unable to get token response', 0, $e);
266
        }
267
268 1
        $params = $this->processResponseParams($client, parse_metadata_response($response));
269
270 1
        return $this->tokenSetFactory->fromArray($params);
271
    }
272
273
    /**
274
     * @param OpenIDClient $client
275
     * @param array<string, mixed> $params
276
     *
277
     * @return array<string, mixed>
0 ignored issues
show
Documentation introduced by
The doc-type array<string, could not be parsed: Expected ">" at position 5, but found "end of type". (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
278
     */
279 1
    private function processResponseParams(OpenIDClient $client, array $params): array
280
    {
281 1
        if (array_key_exists('error', $params)) {
282
            throw OAuth2Exception::fromParameters($params);
283
        }
284
285 1
        if (array_key_exists('response', $params)) {
286
            $params = $this->responseVerifiierBuilder->build($client)
287
                ->verify($params['response']);
288
        }
289
290 1
        if (array_key_exists('error', $params)) {
291
            throw OAuth2Exception::fromParameters($params);
292
        }
293
294 1
        return $params;
295
    }
296
}
297