Completed
Pull Request — master (#12)
by
unknown
04:53
created

Keycloak::getIntrospectTokenUrl()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
namespace Stevenmaguire\OAuth2\Client\Provider;
4
5
use Exception;
6
use Firebase\JWT\JWT;
7
use GuzzleHttp\Client;
8
use GuzzleHttp\RequestOptions;
9
use League\OAuth2\Client\Provider\AbstractProvider;
10
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
11
use League\OAuth2\Client\Token\AccessToken;
12
use League\OAuth2\Client\Tool\BearerAuthorizationTrait;
13
use Psr\Http\Message\ResponseInterface;
14
use Stevenmaguire\OAuth2\Client\Provider\Exception\EncryptionConfigurationException;
15
16
class Keycloak extends AbstractProvider
17
{
18
    use BearerAuthorizationTrait;
19
20
    /**
21
     * Keycloak URL, eg. http://localhost:8080/auth.
22
     *
23
     * @var string
24
     */
25
    public $authServerUrl = null;
26
27
    /**
28
     * Realm name, eg. demo.
29
     *
30
     * @var string
31
     */
32
    public $realm = null;
33
34
    /**
35
     * Encryption algorithm.
36
     *
37
     * You must specify supported algorithms for your application. See
38
     * https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40
39
     * for a list of spec-compliant algorithms.
40
     *
41
     * @var string
42
     */
43
    public $encryptionAlgorithm = null;
44
45
    /**
46
     * Encryption key.
47
     *
48
     * @var string
49
     */
50
    public $encryptionKey = null;
51
52
    /**
53
     * Constructs an OAuth 2.0 service provider.
54
     *
55
     * @param array $options An array of options to set on this provider.
56
     *     Options include `clientId`, `clientSecret`, `redirectUri`, and `state`.
57
     *     Individual providers may introduce more options, as needed.
58
     * @param array $collaborators An array of collaborators that may be used to
59
     *     override this provider's default behavior. Collaborators include
60
     *     `grantFactory`, `requestFactory`, `httpClient`, and `randomFactory`.
61 28
     *     Individual providers may introduce more collaborators, as needed.
62
     */
63 28
    public function __construct(array $options = [], array $collaborators = [])
64 4
    {
65 4
        if (isset($options['encryptionKeyPath'])) {
66
            $this->setEncryptionKeyPath($options['encryptionKeyPath']);
67 28
            unset($options['encryptionKeyPath']);
68 28
        }
69
        parent::__construct($options, $collaborators);
70
    }
71
72
    /**
73
     * Attempts to decrypt the given response.
74
     *
75
     * @param  string|array|null $response
76
     *
77 6
     * @return string|array|null
78
     */
79 6
    public function decryptResponse($response)
80 2
    {
81
        if (!is_string($response)) {
82
            return $response;
83 4
        }
84 2
85 2
        if ($this->usesEncryption()) {
86 2
            return json_decode(
87 2
                json_encode(
88 2
                    JWT::decode(
89 2
                        $response,
90
                        $this->encryptionKey,
91
                        array($this->encryptionAlgorithm)
92 2
                    )
93
                ),
94
                true
95
            );
96 2
        }
97
98
        throw EncryptionConfigurationException::undeterminedEncryption();
99
    }
100
101
    /**
102
     * Get authorization url to begin OAuth flow
103
     *
104 6
     * @return string
105
     */
106 6
    public function getBaseAuthorizationUrl()
107
    {
108
        return $this->getBaseUrlWithRealm().'/protocol/openid-connect/auth';
109
    }
110
111
    /**
112
     * Get access token url to retrieve token
113
     *
114
     * @param  array $params
115
     *
116 12
     * @return string
117
     */
118 12
    public function getBaseAccessTokenUrl(array $params)
119
    {
120
        return $this->getBaseUrlWithRealm().'/protocol/openid-connect/token';
121
    }
122
123
    /**
124
     * Get provider url to fetch user details
125
     *
126
     * @param  AccessToken $token
127
     *
128 6
     * @return string
129
     */
130 6
    public function getResourceOwnerDetailsUrl(AccessToken $token)
131
    {
132
        return $this->getBaseUrlWithRealm().'/protocol/openid-connect/userinfo';
133
    }
134
135
    /**
136
     * Get provider url to fetch introspect token
137
     *
138
     * @param  AccessToken $token
139 2
     *
140
     * @return string
141 2
     */
142 2
    public function getIntrospectTokenUrl(AccessToken $token)
0 ignored issues
show
Unused Code introduced by
The parameter $token is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
143 2
    {
144 2
        return $this->getBaseUrlWithRealm() . '/protocol/openid-connect/token/introspect';
145
    }
146
147
    /**
148
     * Builds the logout URL.
149
     *
150
     * @param array $options
151
     * @return string Authorization URL
152 2
     */
153
    public function getLogoutUrl(array $options = [])
154 2
    {
155
        $base = $this->getBaseLogoutUrl();
156
        $params = $this->getAuthorizationParameters($options);
157
        $query = $this->getAuthorizationQuery($params);
158
        return $this->appendQuery($base, $query);
159
    }
160
161
    /**
162 20
     * Get logout url to logout of session token
163
     *
164 20
     * @return string
165
     */
166
    private function getBaseLogoutUrl()
167
    {
168
        return $this->getBaseUrlWithRealm() . '/protocol/openid-connect/logout';
169
    }
170
171
    private function fetchIntrospectToken(AccessToken $token) {
172
        $data = array(
173
            'form_params' => array(
174
                "token" => $token->getToken(),
175 6
                "client_id" => $this->clientId,
176
                "client_secret" => $this->clientSecret ),
177 6
            RequestOptions::SYNCHRONOUS => true
178
        );
179
180
        $url = $this->getIntrospectTokenUrl($token);
181
182
        $client = new Client();
183
        $response = $client->requestAsync(self::METHOD_POST, $url, $data)->wait();
184
        $parsed = $this->parseResponse($response);
185
        return $parsed;
186
    }
187
188 10
    /**
189
     * Creates base url from provider configuration.
190 10
     *
191 2
     * @return string
192 2
     */
193
    protected function getBaseUrlWithRealm()
194 8
    {
195
        return $this->authServerUrl.'/realms/'.$this->realm;
196
    }
197
198
    /**
199
     * Get the default scopes used by this provider.
200
     *
201
     * This should not be a complete list of all scopes, but the minimum
202
     * required for the provider user interface!
203 4
     *
204
     * @return string[]
205 4
     */
206
    protected function getDefaultScopes()
207
    {
208
        return ['name', 'email'];
209
    }
210
211
    /**
212
     * Check a provider response for errors.
213
     *
214 6
     * @throws IdentityProviderException
215
     * @param  ResponseInterface $response
216 6
     * @param  string $data Parsed response data
217
     * @return void
218 6
     */
219
    protected function checkResponse(ResponseInterface $response, $data)
220 4
    {
221
        if (!empty($data['error'])) {
222
            $error = $data['error'].': '.$data['error_description'];
223
            throw new IdentityProviderException($error, 0, $data);
224
        }
225
    }
226
227
    /**
228
     * Generate a user object from a successful user details request.
229
     *
230 4
     * @param array $response
231
     * @param AccessToken $token
232 4
     * @return KeycloakResourceOwner
233
     */
234 4
    protected function createResourceOwner(array $response, AccessToken $token)
235
    {
236
        return new KeycloakResourceOwner($response);
237
    }
238
239
    /**
240
     * Requests and returns the resource owner of given access token.
241
     *
242
     * @param  AccessToken $token
243
     * @return KeycloakResourceOwner
244 4
     */
245
    public function getResourceOwner(AccessToken $token)
246 4
    {
247
        $response = $this->fetchResourceOwnerDetails($token);
248 4
249
        $response = $this->decryptResponse($response);
250
251
        return $this->createResourceOwner($response, $token);
0 ignored issues
show
Bug introduced by
It seems like $response defined by $this->decryptResponse($response) on line 249 can also be of type null or string; however, Stevenmaguire\OAuth2\Cli...::createResourceOwner() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
252
    }
253
254
    /**
255
     * Requests and returns the resource owner of given access token introspection.
256
     *
257
     * @param  AccessToken $token
258
     * @return KeycloakResourceOwner
259 4
     */
260
    public function getResourceOwnerFromIntrospectedToken(AccessToken $token)
261
    {
262 4
        $parsed = $this->fetchIntrospectToken($token);
263 2
264
        $response = $this->decryptResponse($parsed);
265
266
        return $this->createResourceOwner($response, $token);
0 ignored issues
show
Bug introduced by
It seems like $response defined by $this->decryptResponse($parsed) on line 264 can also be of type null or string; however, Stevenmaguire\OAuth2\Cli...::createResourceOwner() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
267 4
    }
268
269
    /**
270
     * Updates expected encryption algorithm of Keycloak instance.
271
     *
272
     * @param string  $encryptionAlgorithm
273
     *
274
     * @return Keycloak
275 4
     */
276
    public function setEncryptionAlgorithm($encryptionAlgorithm)
277 4
    {
278
        $this->encryptionAlgorithm = $encryptionAlgorithm;
279
280
        return $this;
281
    }
282
283
    /**
284
     * Updates expected encryption key of Keycloak instance.
285
     *
286
     * @param string  $encryptionKey
287
     *
288
     * @return Keycloak
289
     */
290
    public function setEncryptionKey($encryptionKey)
291
    {
292
        $this->encryptionKey = $encryptionKey;
293
294
        return $this;
295
    }
296
297
    /**
298
     * Updates expected encryption key of Keycloak instance to content of given
299
     * file path.
300
     *
301
     * @param string  $encryptionKeyPath
302
     *
303
     * @return Keycloak
304
     */
305
    public function setEncryptionKeyPath($encryptionKeyPath)
306
    {
307
        try {
308
            $this->encryptionKey = file_get_contents($encryptionKeyPath);
309
        } catch (Exception $e) {
310
            // Not sure how to handle this yet.
311
        }
312
313
        return $this;
314
    }
315
316
    /**
317
     * Checks if provider is configured to use encryption.
318
     *
319
     * @return bool
320
     */
321
    public function usesEncryption()
322
    {
323
        return (bool) $this->encryptionAlgorithm && $this->encryptionKey;
324
    }
325
}
326