Completed
Push — master ( 4d4b4a...2860f1 )
by
unknown
12:08 queued 11s
created

Keycloak::parseResponse()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 18
ccs 0
cts 0
cp 0
rs 9.6666
c 0
b 0
f 0
cc 2
nc 2
nop 1
crap 6
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
use UnexpectedValueException;
14
15
class Keycloak extends AbstractProvider
16
{
17
    use BearerAuthorizationTrait;
18
19
    /**
20
     * Keycloak URL, eg. http://localhost:8080/auth.
21
     *
22
     * @var string
23
     */
24
    public $authServerUrl = null;
25
26
    /**
27
     * Realm name, eg. demo.
28
     *
29
     * @var string
30
     */
31
    public $realm = null;
32
33
    /**
34
     * Encryption algorithm.
35
     *
36
     * You must specify supported algorithms for your application. See
37
     * https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40
38
     * for a list of spec-compliant algorithms.
39
     *
40
     * @var string
41
     */
42
    public $encryptionAlgorithm = null;
43
44
    /**
45
     * Encryption key.
46
     *
47
     * @var string
48
     */
49
    public $encryptionKey = null;
50
51
    /**
52
     * Constructs an OAuth 2.0 service provider.
53
     *
54
     * @param array $options An array of options to set on this provider.
55
     *     Options include `clientId`, `clientSecret`, `redirectUri`, and `state`.
56
     *     Individual providers may introduce more options, as needed.
57
     * @param array $collaborators An array of collaborators that may be used to
58
     *     override this provider's default behavior. Collaborators include
59
     *     `grantFactory`, `requestFactory`, `httpClient`, and `randomFactory`.
60
     *     Individual providers may introduce more collaborators, as needed.
61 28
     */
62
    public function __construct(array $options = [], array $collaborators = [])
63 28
    {
64 4
        if (isset($options['encryptionKeyPath'])) {
65 4
            $this->setEncryptionKeyPath($options['encryptionKeyPath']);
66 2
            unset($options['encryptionKeyPath']);
67 28
        }
68 28
        parent::__construct($options, $collaborators);
69
    }
70
71
    /**
72
     * Attempts to decrypt the given response.
73
     *
74
     * @param  string|array|null $response
75
     *
76
     * @return string|array|null
77 6
     */
78
    public function decryptResponse($response)
79 6
    {
80 2
        if (!is_string($response)) {
81
            return $response;
82
        }
83 4
84 2
        if ($this->usesEncryption()) {
85 2
            return json_decode(
86 2
                json_encode(
87 2
                    JWT::decode(
88 2
                        $response,
89 2
                        $this->encryptionKey,
90 1
                        array($this->encryptionAlgorithm)
91 1
                    )
92 1
                ),
93 1
                true
94
            );
95
        }
96 2
97
        throw EncryptionConfigurationException::undeterminedEncryption();
98
    }
99
100
    /**
101
     * Get authorization url to begin OAuth flow
102
     *
103
     * @return string
104 6
     */
105
    public function getBaseAuthorizationUrl()
106 6
    {
107
        return $this->getBaseUrlWithRealm().'/protocol/openid-connect/auth';
108
    }
109
110
    /**
111
     * Get access token url to retrieve token
112
     *
113
     * @param  array $params
114
     *
115
     * @return string
116 12
     */
117
    public function getBaseAccessTokenUrl(array $params)
118 12
    {
119
        return $this->getBaseUrlWithRealm().'/protocol/openid-connect/token';
120
    }
121
122
    /**
123
     * Get provider url to fetch user details
124
     *
125
     * @param  AccessToken $token
126
     *
127
     * @return string
128 6
     */
129
    public function getResourceOwnerDetailsUrl(AccessToken $token)
130 6
    {
131
        return $this->getBaseUrlWithRealm().'/protocol/openid-connect/userinfo';
132
    }
133
134
    /**
135
     * Builds the logout URL.
136
     *
137
     * @param array $options
138
     * @return string Authorization URL
139 2
     */
140
    public function getLogoutUrl(array $options = [])
141 2
    {
142 2
        $base = $this->getBaseLogoutUrl();
143 2
        $params = $this->getAuthorizationParameters($options);
144 2
        $query = $this->getAuthorizationQuery($params);
145
        return $this->appendQuery($base, $query);
146
    }
147
148
    /**
149
     * Get logout url to logout of session token
150
     *
151
     * @return string
152 2
     */
153
    private function getBaseLogoutUrl()
154 2
    {
155
        return $this->getBaseUrlWithRealm() . '/protocol/openid-connect/logout';
156
    }
157
158
    /**
159
     * Creates base url from provider configuration.
160
     *
161
     * @return string
162 20
     */
163
    protected function getBaseUrlWithRealm()
164 20
    {
165
        return $this->authServerUrl.'/realms/'.$this->realm;
166
    }
167
168
    /**
169
     * Get the default scopes used by this provider.
170
     *
171
     * This should not be a complete list of all scopes, but the minimum
172
     * required for the provider user interface!
173
     *
174
     * @return string[]
175 6
     */
176
    protected function getDefaultScopes()
177 6
    {
178
        return ['profile', 'email'];
179
    }
180
181
    /**
182
     * Returns the string that should be used to separate scopes when building
183
     * the URL for requesting an access token.
184
     *
185
     * @return string Scope separator, defaults to ','
186 8
     */
187
    protected function getScopeSeparator()
188 8
    {
189
        return ' ';
190
    }
191
192
193
    /**
194
     * Check a provider response for errors.
195
     *
196
     * @throws IdentityProviderException
197
     * @param  ResponseInterface $response
198
     * @param  string $data Parsed response data
199
     * @return void
200 10
     */
201
    protected function checkResponse(ResponseInterface $response, $data)
202 10
    {
203 2
        if (!empty($data['error'])) {
204 2
            $error = $data['error'].': '.$data['error_description'];
205
            throw new IdentityProviderException($error, 0, $data);
206 8
        }
207
    }
208
209
    /**
210
     * Generate a user object from a successful user details request.
211
     *
212
     * @param array $response
213
     * @param AccessToken $token
214
     * @return KeycloakResourceOwner
215 4
     */
216
    protected function createResourceOwner(array $response, AccessToken $token)
217 4
    {
218
        return new KeycloakResourceOwner($response);
219
    }
220
221
    /**
222
     * Requests and returns the resource owner of given access token.
223
     *
224
     * @param  AccessToken $token
225
     * @return KeycloakResourceOwner
226 6
     * @throws EncryptionConfigurationException
227
     */
228 6
    public function getResourceOwner(AccessToken $token)
229
    {
230 6
        $response = $this->fetchResourceOwnerDetails($token);
231
232 4
        // We are always getting an array. We have to check if it is
233
        // the array we created
234
        if (array_key_exists('jwt', $response)) {
235
            $response = $response['jwt'];
236
        }
237
238
        $response = $this->decryptResponse($response);
239
240
        return $this->createResourceOwner($response, $token);
0 ignored issues
show
Bug introduced by
It seems like $response defined by $this->decryptResponse($response) on line 238 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...
241
    }
242 4
243
    /**
244 4
     * Updates expected encryption algorithm of Keycloak instance.
245
     *
246 4
     * @param string  $encryptionAlgorithm
247
     *
248
     * @return Keycloak
249
     */
250
    public function setEncryptionAlgorithm($encryptionAlgorithm)
251
    {
252
        $this->encryptionAlgorithm = $encryptionAlgorithm;
253
254
        return $this;
255
    }
256 4
257
    /**
258 4
     * Updates expected encryption key of Keycloak instance.
259
     *
260 4
     * @param string  $encryptionKey
261
     *
262
     * @return Keycloak
263
     */
264
    public function setEncryptionKey($encryptionKey)
265
    {
266
        $this->encryptionKey = $encryptionKey;
267
268
        return $this;
269
    }
270
271 4
    /**
272
     * Updates expected encryption key of Keycloak instance to content of given
273
     * file path.
274 4
     *
275 3
     * @param string  $encryptionKeyPath
276
     *
277
     * @return Keycloak
278
     */
279 4
    public function setEncryptionKeyPath($encryptionKeyPath)
280
    {
281
        try {
282
            $this->encryptionKey = file_get_contents($encryptionKeyPath);
283
        } catch (Exception $e) {
284
            // Not sure how to handle this yet.
285
        }
286
287 4
        return $this;
288
    }
289 4
290
    /**
291
     * Checks if provider is configured to use encryption.
292
     *
293
     * @return bool
294
     */
295
    public function usesEncryption()
296
    {
297
        return (bool) $this->encryptionAlgorithm && $this->encryptionKey;
298
    }
299
300
    /**
301
     * Parses the response according to its content-type header.
302
     *
303
     * @throws UnexpectedValueException
304
     * @param  ResponseInterface $response
305
     * @return array
306
     */
307
    protected function parseResponse(ResponseInterface $response)
308
    {
309
        // We have a problem with keycloak when the userinfo responses
310
        // with a jwt token
311
        // Because it just return a jwt as string with the header
312
        // application/jwt
313
        // This can't be parsed to a array
314
        // Dont know why this function only allow an array as return value...
315
        $content = (string) $response->getBody();
316
        $type = $this->getContentType($response);
317
318
        if (strpos($type, 'jwt') !== false) {
319
            // Here we make the temporary array
320
            return ['jwt' => $content];
321
        }
322
323
        return parent::parseResponse($response);
324
    }
325
}
326