Completed
Pull Request — master (#7)
by
unknown
03:13
created

Keycloak::getLogoutUrl()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 7
ccs 2
cts 2
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 5
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 League\OAuth2\Client\Provider\AbstractProvider;
8
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
9
use League\OAuth2\Client\Token\AccessToken;
10
use League\OAuth2\Client\Tool\BearerAuthorizationTrait;
11
use Psr\Http\Message\ResponseInterface;
12
use Stevenmaguire\OAuth2\Client\Provider\Exception\EncryptionConfigurationException;
13
14
class Keycloak extends AbstractProvider
15
{
16
    use BearerAuthorizationTrait;
17
18
    /**
19
     * Keycloak URL, eg. http://localhost:8080/auth.
20
     *
21
     * @var string
22
     */
23
    public $authServerUrl = null;
24
25
    /**
26
     * Realm name, eg. demo.
27
     *
28
     * @var string
29
     */
30
    public $realm = null;
31
32
    /**
33
     * Encryption algorithm.
34
     *
35
     * You must specify supported algorithms for your application. See
36
     * https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40
37
     * for a list of spec-compliant algorithms.
38
     *
39
     * @var string
40
     */
41
    public $encryptionAlgorithm = null;
42
43
    /**
44
     * Encryption key.
45
     *
46
     * @var string
47
     */
48
    public $encryptionKey = null;
49
    /**
50
     * Access Token once authenticated.
51
     *
52
     * @var AccessToken
53
     */
54
    protected $accessToken = null;
55
56
    /**
57
     * @var KeyCloakRoles Any roles obtained from the access token.
58
     */
59
    private $keycloakRoles = null;
60
    /**
61 24
     * @var KeycloakEntitlements
62
     */
63 24
    private $keycloakEntitlements = null;
64 2
65 2
    /**
66 1
     * Constructs an OAuth 2.0 service provider.
67 24
     *
68 24
     * @param array $options An array of options to set on this provider.
69
     *     Options include `clientId`, `clientSecret`, `redirectUri`, and `state`.
70
     *     Individual providers may introduce more options, as needed.
71
     * @param array $collaborators An array of collaborators that may be used to
72
     *     override this provider's default behavior. Collaborators include
73
     *     `grantFactory`, `requestFactory`, `httpClient`, and `randomFactory`.
74
     *     Individual providers may introduce more collaborators, as needed.
75
     */
76
    public function __construct(array $options = [], array $collaborators = [])
77 6
    {
78
        if (isset($options['encryptionKeyPath'])) {
79 6
            $this->setEncryptionKeyPath($options['encryptionKeyPath']);
80 4
            unset($options['encryptionKeyPath']);
81 2
        }
82 1
        parent::__construct($options, $collaborators);
83 2
    }
84 1
85 2
    /**
86 2
     * We need to cache the access token locally allowing for later optional post-processing
87 1
     * by `checkForKeycloakRoles()`
88 1
     *
89 1
     * @param mixed $grant
90 1
     * @param array $options
91 1
     * @return AccessToken
92 2
     */
93
    public function getAccessToken($grant, array $options = [])
94 2
    {
95 1
        $this->accessToken = parent::getAccessToken($grant, $options);
96 1
        return $this->accessToken;
97
    }
98 1
99
    /**
100 4
     * Check for Keycloak-supplied additional fields held by the access token which in turn is inside accessToken.
101
     *
102
     * @return KeyCloakRoles
103
     */
104
    public function getKeycloakRoles()
105
    {
106
        if ($this->accessToken != null && $this->encryptionKey != null && $this->encryptionAlgorithm != null) {
107
            $obj = JWT::decode($this->accessToken, $this->encryptionKey, array($this->encryptionAlgorithm));
108 6
            $this->keycloakRoles = new KeycloakRoles($obj);
109
        }
110 6
        return $this->keycloakRoles;
111 1
    }
112
113
    /**
114
     * Obtain the Keycloak entitlements (permissions) this authenticated user has for this resource (by client-id).
115
     *
116
     * This uses the Entitlement API offered by Keycloak.
117
     * @return KeycloakEntitlements Entitlements in a convenient wrapper model
118
     */
119
    public function getKeycloakEntitlements()
120 12
    {
121
        if ($this->keycloakEntitlements == null) {
122 12
            $request = $this->getAuthenticatedRequest(
123 1
                'GET',
124
                $this->getEntitlementsUrl($this->accessToken),
125
                $this->accessToken,
126
                []
127
            );
128
            $response = $this->getParsedResponse($request);
129
            // Should have an rpt field
130
            $entitlements = JWT::decode(
131
                $response['rpt'],
132 6
                $this->encryptionKey,
133
                [$this->encryptionAlgorithm]
134 6
            );
135
            $this->keycloakEntitlements = new KeycloakEntitlements($entitlements);
136
        }
137
138
        return $this->keycloakEntitlements;
139
    }
140
141
    /**
142 18
     * Attempts to decrypt the given response.
143
     *
144 18
     * @param  string|array|null $response
145
     * @return array|null|string
146
     * @throws EncryptionConfigurationException
147
     */
148
    public function decryptResponse($response)
149
    {
150
        if (is_string($response)) {
151
            if ($this->encryptionAlgorithm && $this->encryptionKey) {
152
                $response = json_decode(
153
                    json_encode(
154
                        JWT::decode(
155 4
                            $response,
156
                            $this->encryptionKey,
157 4
                            array($this->encryptionAlgorithm)
158
                        )
159
                    ),
160
                    true
161
                );
162
            } else {
163
                throw new EncryptionConfigurationException(
164
                    'The given response may be encrypted and sufficient ' .
165
                    'encryption configuration has not been provided.',
166
                    400
167
                );
168 10
            }
169
        }
170 10
171 2
        return $response;
172 2
    }
173
174 8
    /**
175
     * Builds the logout URL.
176
     *
177
     * @param array $options
178
     * @return string Authorization URL
179
     */
180
    public function getLogoutUrl(array $options = [])
181
    {
182
        $base   = $this->getBaseLogoutUrl();
183 4
        $params = $this->getAuthorizationParameters($options);
184
        $query  = $this->getAuthorizationQuery($params);
185 4
        return $this->appendQuery($base, $query);
186
    }
187
188
    /**
189
     * Get logout url to logout of session token
190
     *
191
     * @return string
192
     */
193
    public function getBaseLogoutUrl()
194 6
    {
195
        return $this->getBaseUrlWithRealm().'/protocol/openid-connect/logout';
196 6
    }
197
198 6
    /**
199
     * Get authorization url to begin OAuth flow
200 4
     *
201
     * @return string
202
     */
203
    public function getBaseAuthorizationUrl()
204
    {
205
        return $this->getBaseUrlWithRealm() . '/protocol/openid-connect/auth';
206
    }
207
208
    /**
209
     * Get access token url to retrieve token
210 4
     *
211
     * @param  array $params
212 4
     *
213
     * @return string
214 4
     */
215
    public function getBaseAccessTokenUrl(array $params)
216
    {
217
        return $this->getBaseUrlWithRealm() . '/protocol/openid-connect/token';
218
    }
219
220
    /**
221
     * Get provider url to fetch user details
222
     *
223
     * @param  AccessToken $token
224 4
     *
225
     * @return string
226 4
     */
227
    public function getResourceOwnerDetailsUrl(AccessToken $token)
228 4
    {
229
        return $this->getBaseUrlWithRealm() . '/protocol/openid-connect/userinfo';
230
    }
231
232
    /**
233
     * Keycloak extension supporting entitlements.
234
     *
235
     * @param AccessToken $token
236
     * @return string
237
     */
238
    public function getEntitlementsUrl(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...
239 2
    {
240
        return $this->getBaseUrlWithRealm() . '/authz/entitlement/' . $this->clientId;
241
    }
242 2
243 1
    /**
244
     * Creates base url from provider configuration.
245
     *
246
     * @return string
247 2
     */
248
    protected function getBaseUrlWithRealm()
249
    {
250
        return $this->authServerUrl . '/realms/' . $this->realm;
251
    }
252
253
    /**
254
     * Get the default scopes used by this provider.
255
     *
256
     * This should not be a complete list of all scopes, but the minimum
257
     * required for the provider user interface!
258
     *
259
     * @return string[]
260
     */
261
    protected function getDefaultScopes()
262
    {
263
        return ['name', 'email'];
264
    }
265
266
    protected function getAuthorizationHeaders($token = null)
267
    {
268
        $headers = parent::getAuthorizationHeaders($token);
269
        if ($token != null) {
270
            $headers['Authorization'] = 'Bearer ' . $token;
271
        }
272
        return $headers;
273
    }
274
275
    /**
276
     * Check a provider response for errors.
277
     *
278
     * @throws IdentityProviderException
279
     * @param  ResponseInterface $response
280
     * @param  string $data Parsed response data
281
     * @return void
282
     */
283
    protected function checkResponse(ResponseInterface $response, $data)
284
    {
285
        if (!empty($data['error'])) {
286
            $error = $data['error'] . ': ' . $data['error_description'];
287
            throw new IdentityProviderException($error, 0, $data);
288
        }
289
    }
290
291
    /**
292
     * Generate a user object from a successful user details request.
293
     *
294
     * @param array $response
295
     * @param AccessToken $token
296
     * @return KeycloakResourceOwner
297
     */
298
    protected function createResourceOwner(array $response, AccessToken $token)
299
    {
300
        return new KeycloakResourceOwner($response);
301
    }
302
303
    /**
304
     * Requests and returns the resource owner of given access token.
305
     *
306
     * @param  AccessToken $token
307
     * @return KeycloakResourceOwner
308
     */
309
    public function getResourceOwner(AccessToken $token)
310
    {
311
        $response = $this->fetchResourceOwnerDetails($token);
312
313
        $response = $this->decryptResponse($response);
314
315
        return $this->createResourceOwner($response, $token);
0 ignored issues
show
Bug introduced by
It seems like $response defined by $this->decryptResponse($response) on line 313 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...
316
    }
317
318
    /**
319
     * Updates expected encryption algorithm of Keycloak instance.
320
     *
321
     * @param string $encryptionAlgorithm
322
     *
323
     * @return Keycloak
324
     */
325
    public function setEncryptionAlgorithm($encryptionAlgorithm)
326
    {
327
        $this->encryptionAlgorithm = $encryptionAlgorithm;
328
329
        return $this;
330
    }
331
332
    /**
333
     * Updates expected encryption key of Keycloak instance.
334
     *
335
     * @param string $encryptionKey
336
     *
337
     * @return Keycloak
338
     */
339
    public function setEncryptionKey($encryptionKey)
340
    {
341
        $this->encryptionKey = $encryptionKey;
342
343
        return $this;
344
    }
345
346
    /**
347
     * Updates expected encryption key of Keycloak instance to content of given
348
     * file path.
349
     *
350
     * @param string $encryptionKeyPath
351
     *
352
     * @return Keycloak
353
     */
354
    public function setEncryptionKeyPath($encryptionKeyPath)
355
    {
356
        try {
357
            $this->encryptionKey = file_get_contents($encryptionKeyPath);
358
        } catch (Exception $e) {
359
            // Not sure how to handle this yet.
360
        }
361
362
        return $this;
363
    }
364
}
365