Completed
Pull Request — master (#18)
by
unknown
13:45
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 Firebase\JWT\JWT;
6
use League\OAuth2\Client\Provider\AbstractProvider;
7
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
8
use League\OAuth2\Client\Token\AccessToken;
9
use League\OAuth2\Client\Tool\BearerAuthorizationTrait;
10
use Psr\Http\Message\ResponseInterface;
11
use Stevenmaguire\OAuth2\Client\Provider\Exception\EncryptionConfigurationException;
12
use Stevenmaguire\OAuth2\Client\Provider\Exception\EncryptionKeyPathNotFoundException;
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
     * @throws EncryptionKeyPathNotFoundException
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
     * @throws EncryptionConfigurationException
79 6
     */
80 2
    public function decryptResponse($response)
81
    {
82
        if (!is_string($response)) {
83 4
            return $response;
84 2
        }
85 2
86 2
        if ($this->usesEncryption()) {
87 2
            return json_decode(
88 2
                json_encode(
89 2
                    JWT::decode(
90
                        $response,
91
                        $this->encryptionKey,
92 2
                        array($this->encryptionAlgorithm)
93
                    )
94
                ),
95
                true
96 2
            );
97
        }
98
99
        throw EncryptionConfigurationException::undeterminedEncryption();
100
    }
101
102
    /**
103
     * Get authorization url to begin OAuth flow
104 6
     *
105
     * @return string
106 6
     */
107
    public function getBaseAuthorizationUrl()
108
    {
109
        return $this->getBaseUrlWithRealm().'/protocol/openid-connect/auth';
110
    }
111
112
    /**
113
     * Get access token url to retrieve token
114
     *
115
     * @param  array $params
116 12
     *
117
     * @return string
118 12
     */
119
    public function getBaseAccessTokenUrl(array $params)
120
    {
121
        return $this->getBaseUrlWithRealm().'/protocol/openid-connect/token';
122
    }
123
124
    /**
125
     * Get provider url to fetch user details
126
     *
127
     * @param  AccessToken $token
128 6
     *
129
     * @return string
130 6
     */
131
    public function getResourceOwnerDetailsUrl(AccessToken $token)
132
    {
133
        return $this->getBaseUrlWithRealm().'/protocol/openid-connect/userinfo';
134
    }
135
136
    /**
137
     * Builds the logout URL.
138
     *
139 2
     * @param array $options
140
     * @return string Authorization URL
141 2
     */
142 2
    public function getLogoutUrl(array $options = [])
143 2
    {
144 2
        $base = $this->getBaseLogoutUrl();
145
        $params = $this->getAuthorizationParameters($options);
146
        $query = $this->getAuthorizationQuery($params);
147
        return $this->appendQuery($base, $query);
148
    }
149
150
    /**
151
     * Get logout url to logout of session token
152 2
     *
153
     * @return string
154 2
     */
155
    private function getBaseLogoutUrl()
156
    {
157
        return $this->getBaseUrlWithRealm() . '/protocol/openid-connect/logout';
158
    }
159
160
    /**
161
     * Creates base url from provider configuration.
162 20
     *
163
     * @return string
164 20
     */
165
    protected function getBaseUrlWithRealm()
166
    {
167
        return $this->authServerUrl.'/realms/'.$this->realm;
168
    }
169
170
    /**
171
     * Get the default scopes used by this provider.
172
     *
173
     * This should not be a complete list of all scopes, but the minimum
174
     * required for the provider user interface!
175 6
     *
176
     * @return string[]
177 6
     */
178
    protected function getDefaultScopes()
179
    {
180
        return ['name', 'email'];
181
    }
182
183
    /**
184
     * Check a provider response for errors.
185
     *
186
     * @throws IdentityProviderException
187
     * @param  ResponseInterface $response
188 10
     * @param  string $data Parsed response data
189
     * @return void
190 10
     */
191 2
    protected function checkResponse(ResponseInterface $response, $data)
192 2
    {
193
        if (is_array($data) && !empty($data['error'])) {
194 8
            $error = $data['error'].': '.$data['error_description'];
195
            throw new IdentityProviderException($error, 0, $data);
196
        }
197
    }
198
199
    /**
200
     * Generate a user object from a successful user details request.
201
     *
202
     * @param array $response
203 4
     * @param AccessToken $token
204
     * @return KeycloakResourceOwner
205 4
     */
206
    protected function createResourceOwner(array $response, AccessToken $token)
207
    {
208
        return new KeycloakResourceOwner($response);
209
    }
210
211
    /**
212
     * Requests and returns the resource owner of given access token.
213
     *
214 6
     * @param  AccessToken $token
215
     * @return KeycloakResourceOwner
216 6
     * @throws EncryptionConfigurationException
217
     */
218 6
    public function getResourceOwner(AccessToken $token)
219
    {
220 4
        $response = $this->fetchResourceOwnerDetails($token);
221
222
        // We are always getting an array. We have to check if it is
223
        // the array we created
224
        if (array_key_exists('jwt', $response)) {
225
            $response = $response['jwt'];
226
        }
227
228
        $response = $this->decryptResponse($response);
229
230 4
        return $this->createResourceOwner($response, $token);
0 ignored issues
show
Bug introduced by
It seems like $response defined by $this->decryptResponse($response) on line 228 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...
231
    }
232 4
233
    /**
234 4
     * Updates expected encryption algorithm of Keycloak instance.
235
     *
236
     * @param string  $encryptionAlgorithm
237
     *
238
     * @return Keycloak
239
     */
240
    public function setEncryptionAlgorithm($encryptionAlgorithm)
241
    {
242
        $this->encryptionAlgorithm = $encryptionAlgorithm;
243
244 4
        return $this;
245
    }
246 4
247
    /**
248 4
     * Updates expected encryption key of Keycloak instance.
249
     *
250
     * @param string  $encryptionKey
251
     *
252
     * @return Keycloak
253
     */
254
    public function setEncryptionKey($encryptionKey)
255
    {
256
        $this->encryptionKey = $encryptionKey;
257
258
        return $this;
259 4
    }
260
261
    /**
262 4
     * Updates expected encryption key of Keycloak instance to content of given
263 2
     * file path.
264
     *
265
     * @param string $encryptionKeyPath
266
     *
267 4
     * @return Keycloak
268
     * @throws EncryptionKeyPathNotFoundException
269
     */
270
    public function setEncryptionKeyPath($encryptionKeyPath)
271
    {
272
        try {
273
            $this->encryptionKey = file_get_contents($encryptionKeyPath);
274
        } catch (\Throwable $e) {
275 4
            $message = 'Could not find the encryption key path: "'. $encryptionKeyPath . '"';
276
            throw new EncryptionKeyPathNotFoundException($message, 0, $e);
277 4
        }
278
279
        return $this;
280
    }
281
282
    /**
283
     * Checks if provider is configured to use encryption.
284
     *
285
     * @return bool
286
     */
287
    public function usesEncryption()
288
    {
289
        return (bool) $this->encryptionAlgorithm && $this->encryptionKey;
290
    }
291
292
    /**
293
     * Parses the response according to its content-type header.
294
     *
295
     * @throws UnexpectedValueException
296
     * @param  ResponseInterface $response
297
     * @return array
298
     */
299
    protected function parseResponse(ResponseInterface $response)
300
    {
301
        // We have a problem with keycloak when the userinfo responses
302
        // with a jwt token
303
        // Because it just return a jwt as string with the header
304
        // application/jwt
305
        // This can't be parsed to a array
306
        // Dont know why this function only allow an array as return value...
307
        $content = (string) $response->getBody();
308
        $type = $this->getContentType($response);
309
310
        if (strpos($type, 'jwt') !== false) {
311
            // Here we make the temporary array
312
            return ['jwt' => $content];
313
        }
314
315
        return parent::parseResponse($response);
316
    }
317
}
318