AuthorizationService   A
last analyzed

Complexity

Total Complexity 35

Size/Duplication

Total Lines 210
Duplicated Lines 0 %

Test Coverage

Coverage 47.31%

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 92
c 4
b 0
f 0
dl 0
loc 210
ccs 44
cts 93
cp 0.4731
rs 9.6
wmc 35

8 Methods

Rating   Name   Duplication   Size   Complexity  
A callback() 0 22 3
A refresh() 0 17 2
B __construct() 0 16 8
B getAuthorizationUri() 0 33 9
A getCallbackParams() 0 3 1
A fetchToken() 0 37 5
A grant() 0 23 3
A processResponseParams() 0 16 4
1
<?php
2
3
declare(strict_types=1);
4
5
namespace TMV\OpenIdClient\Service;
6
7
use function array_filter;
8
use function array_key_exists;
9
use function array_merge;
10
use Http\Discovery\Psr17FactoryDiscovery;
11
use Http\Discovery\Psr18ClientDiscovery;
12
use function http_build_query;
13
use function is_array;
14
use function is_string;
15
use function json_encode;
16
use JsonSerializable;
17
use Psr\Http\Client\ClientExceptionInterface;
18
use Psr\Http\Client\ClientInterface;
19
use Psr\Http\Message\RequestFactoryInterface;
20
use Psr\Http\Message\ServerRequestInterface;
21
use Psr\Http\Message\UriFactoryInterface;
22
use TMV\OpenIdClient\Client\ClientInterface as OpenIDClient;
23
use TMV\OpenIdClient\Exception\InvalidArgumentException;
24
use TMV\OpenIdClient\Exception\OAuth2Exception;
25
use TMV\OpenIdClient\Exception\RuntimeException;
26
use function TMV\OpenIdClient\get_endpoint_uri;
27
use function TMV\OpenIdClient\parse_callback_params;
28
use function TMV\OpenIdClient\parse_metadata_response;
29
use TMV\OpenIdClient\Session\AuthSessionInterface;
30
use TMV\OpenIdClient\Token\ResponseTokenVerifier;
31
use TMV\OpenIdClient\Token\ResponseTokenVerifierInterface;
32
use TMV\OpenIdClient\Token\TokenDecrypter;
33
use TMV\OpenIdClient\Token\TokenDecrypterInterface;
34
use TMV\OpenIdClient\Token\TokenSetFactory;
35
use TMV\OpenIdClient\Token\TokenSetFactoryInterface;
36
use TMV\OpenIdClient\Token\TokenSetInterface;
37
use TMV\OpenIdClient\Token\TokenSetVerifier;
38
use TMV\OpenIdClient\Token\TokenSetVerifierInterface;
39
40
/**
41
 * OAuth 2.0
42
 *
43
 * @link https://tools.ietf.org/html/rfc6749 RFC 6749
44
 */
45
class AuthorizationService
46
{
47
    /** @var TokenSetFactoryInterface */
48
    private $tokenSetFactory;
49
50
    /** @var TokenSetVerifierInterface */
51
    private $tokenSetVerifier;
52
53
    /** @var ResponseTokenVerifierInterface */
54
    private $responseTokenVerifier;
55
56
    /** @var TokenDecrypterInterface */
57
    private $idTokenDecrypter;
58
59
    /** @var ClientInterface */
60
    private $client;
61
62
    /** @var RequestFactoryInterface */
63
    private $requestFactory;
64
65
    /** @var UriFactoryInterface */
66
    private $uriFactory;
67
68 2
    public function __construct(
69
        ?TokenSetFactoryInterface $tokenSetFactory = null,
70
        ?TokenSetVerifierInterface $tokenSetVerifier = null,
71
        ?ResponseTokenVerifierInterface $responseTokenVerifier = null,
72
        ?TokenDecrypterInterface $idTokenDecrypter = null,
73
        ?ClientInterface $client = null,
74
        ?RequestFactoryInterface $requestFactory = null,
75
        ?UriFactoryInterface $uriFactory = null
76
    ) {
77 2
        $this->tokenSetFactory = $tokenSetFactory ?: new TokenSetFactory();
78 2
        $this->tokenSetVerifier = $tokenSetVerifier ?: new TokenSetVerifier();
79 2
        $this->responseTokenVerifier = $responseTokenVerifier ?: new ResponseTokenVerifier();
80 2
        $this->idTokenDecrypter = $idTokenDecrypter ?: new TokenDecrypter();
81 2
        $this->client = $client ?: Psr18ClientDiscovery::find();
82 2
        $this->requestFactory = $requestFactory ?: Psr17FactoryDiscovery::findRequestFactory();
83 2
        $this->uriFactory = $uriFactory ?: Psr17FactoryDiscovery::findUrlFactory();
84 2
    }
85
86
    /**
87
     * @param OpenIDClient $client
88
     * @param array<string, mixed> $params
89
     *
90
     * @return string
91
     */
92 1
    public function getAuthorizationUri(OpenIDClient $client, array $params = []): string
93
    {
94 1
        $clientMetadata = $client->getMetadata();
95 1
        $issuerMetadata = $client->getIssuer()->getMetadata();
96 1
        $endpointUri = $issuerMetadata->getAuthorizationEndpoint();
97
98 1
        $params = array_merge([
99 1
            'client_id' => $clientMetadata->getClientId(),
100 1
            'scope' => 'openid',
101 1
            'response_type' => $clientMetadata->getResponseTypes()[0] ?? 'code',
102 1
            'redirect_uri' => $clientMetadata->getRedirectUris()[0] ?? null,
103 1
        ], $params);
104
105
        $params = array_filter($params, static function ($value) {
106 1
            return null !== $value;
107 1
        });
108
109 1
        foreach ($params as $key => $value) {
110 1
            if (null === $value) {
111
                unset($params[$key]);
112 1
            } elseif ('claims' === $key && (is_array($value) || $value instanceof JsonSerializable)) {
113
                $params['claims'] = json_encode($value);
114 1
            } elseif (! is_string($value)) {
115
                $params[$key] = (string) $value;
116
            }
117
        }
118
119 1
        if (! array_key_exists('nonce', $params) && 'code' !== ($params['response_type'] ?? '')) {
120
            throw new InvalidArgumentException('nonce MUST be provided for implicit and hybrid flows');
121
        }
122
123 1
        return (string) $this->uriFactory->createUri($endpointUri)
124 1
            ->withQuery(http_build_query($params));
125
    }
126
127
    public function getCallbackParams(ServerRequestInterface $serverRequest, OpenIDClient $client): array
128
    {
129
        return $this->processResponseParams($client, parse_callback_params($serverRequest));
130
    }
131
132
    public function callback(
133
        OpenIDClient $client,
134
        array $params,
135
        ?string $redirectUri = null,
136
        ?AuthSessionInterface $authSession = null,
137
        ?int $maxAge = null
138
    ): TokenSetInterface {
139
        $tokenSet = $this->tokenSetFactory->fromArray($params);
140
141
        $idToken = $tokenSet->getIdToken();
142
143
        if (null !== $idToken) {
144
            $tokenSet = $tokenSet->withIdToken($this->idTokenDecrypter->decryptToken($client, $idToken));
145
            $this->tokenSetVerifier->validate($tokenSet, $client, $authSession, true, $maxAge);
146
        }
147
148
        if (null === $tokenSet->getCode()) {
149
            return $tokenSet;
150
        }
151
152
        // get token
153
        return $this->fetchToken($client, $tokenSet, $redirectUri, $authSession, $maxAge);
154
    }
155
156
    public function fetchToken(
157
        OpenIDClient $client,
158
        TokenSetInterface $tokenSet,
159
        ?string $redirectUri = null,
160
        ?AuthSessionInterface $authSession = null,
161
        ?int $maxAge = null
162
    ): TokenSetInterface {
163
        $code = $tokenSet->getCode();
164
165
        if (null === $code) {
166
            throw new RuntimeException('Unable to fetch token without a code');
167
        }
168
169
        if (null === $redirectUri) {
170
            $redirectUri = $client->getMetadata()->getRedirectUris()[0] ?? null;
171
        }
172
173
        if (null === $redirectUri) {
174
            throw new InvalidArgumentException('A redirect_uri should be provided');
175
        }
176
177
        $tokenSet = $this->grant($client, [
178
            'grant_type' => 'authorization_code',
179
            'code' => $code,
180
            'redirect_uri' => $redirectUri,
181
        ]);
182
183
        $idToken = $tokenSet->getIdToken();
184
185
        if (null === $idToken) {
186
            return $tokenSet;
187
        }
188
189
        $tokenSet = $tokenSet->withIdToken($this->idTokenDecrypter->decryptToken($client, $idToken));
190
        $this->tokenSetVerifier->validate($tokenSet, $client, $authSession, false, $maxAge);
191
192
        return $tokenSet;
193
    }
194
195
    public function refresh(OpenIDClient $client, string $refreshToken, array $params = []): TokenSetInterface
196
    {
197
        $tokenSet = $this->grant($client, array_merge($params, [
198
            'grant_type' => 'refresh_token',
199
            'refresh_token' => $refreshToken,
200
        ]));
201
202
        $idToken = $tokenSet->getIdToken();
203
204
        if (null === $idToken) {
205
            return $tokenSet;
206
        }
207
208
        $tokenSet = $tokenSet->withIdToken($this->idTokenDecrypter->decryptToken($client, $idToken));
209
        $this->tokenSetVerifier->validate($tokenSet, $client, null, false);
210
211
        return $tokenSet;
212
    }
213
214 1
    public function grant(OpenIDClient $client, array $params = []): TokenSetInterface
215
    {
216 1
        $authMethod = $client->getAuthMethodFactory()
217 1
            ->create($client->getMetadata()->getTokenEndpointAuthMethod());
218
219 1
        $endpointUri = get_endpoint_uri($client, 'token_endpoint');
220
221 1
        $tokenRequest = $this->requestFactory->createRequest('POST', $endpointUri)
222 1
            ->withHeader('content-type', 'application/x-www-form-urlencoded');
223
224 1
        $tokenRequest = $authMethod->createRequest($tokenRequest, $client, $params);
225
226 1
        $httpClient = $client->getHttpClient() ?: $this->client;
227
228
        try {
229 1
            $response = $httpClient->sendRequest($tokenRequest);
230
        } catch (ClientExceptionInterface $e) {
231
            throw new RuntimeException('Unable to get token response', 0, $e);
232
        }
233
234 1
        $params = $this->processResponseParams($client, parse_metadata_response($response));
235
236 1
        return $this->tokenSetFactory->fromArray($params);
237
    }
238
239 1
    private function processResponseParams(OpenIDClient $client, array $params): array
240
    {
241 1
        if (array_key_exists('error', $params)) {
242
            throw OAuth2Exception::fromParameters($params);
243
        }
244
245 1
        if (array_key_exists('response', $params)) {
246
            $decrypted = $this->idTokenDecrypter->decryptToken($client, $params['response']);
247
            $params = $this->responseTokenVerifier->validate($client, $decrypted);
248
        }
249
250 1
        if (array_key_exists('error', $params)) {
251
            throw OAuth2Exception::fromParameters($params);
252
        }
253
254 1
        return $params;
255
    }
256
}
257