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

Keycloak::getAuthorizationHeaders()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 8
ccs 6
cts 6
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 5
nc 2
nop 1
crap 2
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
     * @var KeycloakEntitlements
62
     */
63
    private $keycloakEntitlements = null;
64
65
    /**
66
     * Constructs an OAuth 2.0 service provider.
67
     *
68
     * @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 34
    public function __construct(array $options = [], array $collaborators = [])
77
    {
78 34
        if (isset($options['encryptionKeyPath'])) {
79 2
            $this->setEncryptionKeyPath($options['encryptionKeyPath']);
80 2
            unset($options['encryptionKeyPath']);
81 1
        }
82 34
        parent::__construct($options, $collaborators);
83 34
    }
84
85
    /**
86
     * We need to cache the access token locally allowing for later optional post-processing by `checkForKeycloakRoles()`
87
     *
88
     * @param mixed $grant
89
     * @param array $options
90
     * @return AccessToken
91
     */
92 10
    public function getAccessToken($grant, array $options = [])
93
    {
94 10
        $this->accessToken = parent::getAccessToken($grant, $options);
95 8
        return $this->accessToken;
96
    }
97
98
    /**
99
     * Check for Keycloak-supplied additional fields held by the access token which in turn is inside accessToken.
100
     *
101
     */
102
    public function checkForKeycloakRoles() {
103
        if ($this->accessToken != null && $this->encryptionKey != null && $this->encryptionAlgorithm != null) {
104
            $this->keycloakRoles = KeycloakRoles::fromToken($this->accessToken, $this->encryptionKey, $this->encryptionAlgorithm);
105
        }
106
    }
107
108
    /**
109
     * @return KeyCloakRoles
110
     */
111
    public function getKeycloakRoles()
112
    {
113
        return $this->keycloakRoles;
114
    }
115
116
    /**
117
     * Obtain the entitlements (permissions) this authenticated user has for this resource (by client-id).
118
     *
119
     * This uses the Entitlement API offered by Keycloak.
120
     * @return KeycloakEntitlements Entitlements in a convenient wrapper model
121
     */
122 1
    public function getEntitlements() {
123 1
        if ($this->keycloakEntitlements == null) {
124
            $request = $this->getAuthenticatedRequest('GET', $this->getEntitlementsUrl($this->accessToken), $this->accessToken, []);
125
            $response = $this->getParsedResponse($request);
126
            // Should have an rpt field
127
            $entitlements = JWT::decode($response['rpt'], $this->encryptionKey, [$this->encryptionAlgorithm]);
128
            $this->keycloakEntitlements = new KeycloakEntitlements($entitlements);
129
        }
130
131
        return $this->keycloakEntitlements;
132
    }
133
134
    /**
135
     * Attempts to decrypt the given response.
136
     *
137
     * @param  string|array|null $response
138
     * @return array|null|string
139
     * @throws EncryptionConfigurationException
140
     */
141 6
    public function decryptResponse($response)
142
    {
143 6
        if (is_string($response)) {
144 4
            if ($this->encryptionAlgorithm && $this->encryptionKey) {
145 2
                $response = json_decode(
146 2
                    json_encode(
147 2
                        JWT::decode(
148 2
                            $response,
149 2
                            $this->encryptionKey,
150 2
                            array($this->encryptionAlgorithm)
151 1
                        )
152 1
                    ),
153 1
                    true
154 1
                );
155 1
            } else {
156 2
                throw new EncryptionConfigurationException(
157
                    'The given response may be encrypted and sufficient '.
158 2
                    'encryption configuration has not been provided.',
159 1
                    400
160 1
                );
161
            }
162 1
        }
163
164 4
        return $response;
165
    }
166
167
    /**
168
     * Get authorization url to begin OAuth flow
169
     *
170
     * @return string
171
     */
172 6
    public function getBaseAuthorizationUrl()
173
    {
174 6
        return $this->getBaseUrlWithRealm().'/protocol/openid-connect/auth';
175
    }
176
177
    /**
178
     * Get access token url to retrieve token
179
     *
180
     * @param  array $params
181
     *
182
     * @return string
183
     */
184 12
    public function getBaseAccessTokenUrl(array $params)
185
    {
186 12
        return $this->getBaseUrlWithRealm().'/protocol/openid-connect/token';
187
    }
188
189
    /**
190
     * Get provider url to fetch user details
191
     *
192
     * @param  AccessToken $token
193
     *
194
     * @return string
195
     */
196 6
    public function getResourceOwnerDetailsUrl(AccessToken $token)
197
    {
198 6
        return $this->getBaseUrlWithRealm().'/protocol/openid-connect/userinfo';
199
    }
200
201
    /**
202
     * Keycloak extension supporting entitlements.
203
     *
204
     * @param AccessToken $token
205
     * @return string
206
     */
207
    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...
208
        return $this->getBaseUrlWithRealm().'/authz/entitlement/'.$this->clientId;
209
    }
210
211
    /**
212
     * Creates base url from provider configuration.
213
     *
214
     * @return string
215
     */
216 18
    protected function getBaseUrlWithRealm()
217
    {
218 18
        return $this->authServerUrl.'/realms/'.$this->realm;
219
    }
220
221
    /**
222
     * Get the default scopes used by this provider.
223
     *
224
     * This should not be a complete list of all scopes, but the minimum
225
     * required for the provider user interface!
226
     *
227
     * @return string[]
228
     */
229 4
    protected function getDefaultScopes()
230
    {
231 4
        return ['name', 'email'];
232
    }
233
234 6
    protected function getAuthorizationHeaders($token = null)
235
    {
236 6
        $headers = parent::getAuthorizationHeaders($token);
237 6
        if ($token != null) {
238 6
            $headers['Authorization'] = 'Bearer ' . $token;
239 3
        }
240 6
        return $headers;
241
    }
242
243
    /**
244
     * Check a provider response for errors.
245
     *
246
     * @throws IdentityProviderException
247
     * @param  ResponseInterface $response
248
     * @param  string $data Parsed response data
249
     * @return void
250
     */
251 10
    protected function checkResponse(ResponseInterface $response, $data)
252
    {
253 10
        if (!empty($data['error'])) {
254 2
            $error = $data['error'].': '.$data['error_description'];
255 2
            throw new IdentityProviderException($error, 0, $data);
256
        }
257 8
    }
258
259
    /**
260
     * Generate a user object from a successful user details request.
261
     *
262
     * @param array $response
263
     * @param AccessToken $token
264
     * @return KeycloakResourceOwner
265
     */
266 4
    protected function createResourceOwner(array $response, AccessToken $token)
267
    {
268 4
        return new KeycloakResourceOwner($response);
269
    }
270
271
    /**
272
     * Requests and returns the resource owner of given access token.
273
     *
274
     * @param  AccessToken $token
275
     * @return KeycloakResourceOwner
276
     */
277 6
    public function getResourceOwner(AccessToken $token)
278
    {
279 6
        $response = $this->fetchResourceOwnerDetails($token);
280
281 6
        $response = $this->decryptResponse($response);
282
283 4
        return $this->createResourceOwner($response, $token);
0 ignored issues
show
Bug introduced by
It seems like $response defined by $this->decryptResponse($response) on line 281 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...
284
    }
285
286
    /**
287
     * Updates expected encryption algorithm of Keycloak instance.
288
     *
289
     * @param string  $encryptionAlgorithm
290
     *
291
     * @return Keycloak
292
     */
293 4
    public function setEncryptionAlgorithm($encryptionAlgorithm)
294
    {
295 4
        $this->encryptionAlgorithm = $encryptionAlgorithm;
296
297 4
        return $this;
298
    }
299
300
    /**
301
     * Updates expected encryption key of Keycloak instance.
302
     *
303
     * @param string  $encryptionKey
304
     *
305
     * @return Keycloak
306
     */
307 4
    public function setEncryptionKey($encryptionKey)
308
    {
309 4
        $this->encryptionKey = $encryptionKey;
310
311 4
        return $this;
312
    }
313
314
    /**
315
     * Updates expected encryption key of Keycloak instance to content of given
316
     * file path.
317
     *
318
     * @param string  $encryptionKeyPath
319
     *
320
     * @return Keycloak
321
     */
322 2
    public function setEncryptionKeyPath($encryptionKeyPath)
323
    {
324
        try {
325 2
            $this->encryptionKey = file_get_contents($encryptionKeyPath);
326 1
        } catch (Exception $e) {
327
            // Not sure how to handle this yet.
328
        }
329
330 2
        return $this;
331
    }
332
}
333