Completed
Pull Request — master (#17)
by
unknown
10:42
created

Keycloak::decryptResponse()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 21
ccs 11
cts 11
cp 1
rs 9.584
c 0
b 0
f 0
cc 3
nc 3
nop 1
crap 3
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
            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
                        array($this->encryptionAlgorithm)
91
                    )
92 2
                ),
93
                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 ['name', 'email'];
179
    }
180
181
    /**
182
     * Check a provider response for errors.
183
     *
184
     * @throws IdentityProviderException
185
     * @param  ResponseInterface $response
186
     * @param  string $data Parsed response data
187
     * @return void
188 10
     */
189
    protected function checkResponse(ResponseInterface $response, $data)
190 10
    {
191 2
        if (!empty($data['error'])) {
192 2
            $error = $data['error'].': '.$data['error_description'];
193
            throw new IdentityProviderException($error, 0, $data);
194 8
        }
195
    }
196
197
    /**
198
     * Generate a user object from a successful user details request.
199
     *
200
     * @param array $response
201
     * @param AccessToken $token
202
     * @return KeycloakResourceOwner
203 4
     */
204
    protected function createResourceOwner(array $response, AccessToken $token)
205 4
    {
206
        return new KeycloakResourceOwner($response);
207
    }
208
209
    /**
210
     * Requests and returns the resource owner of given access token.
211
     *
212
     * @param  AccessToken $token
213
     * @return KeycloakResourceOwner
214 6
     * @throws EncryptionConfigurationException
215
     */
216 6
    public function getResourceOwner(AccessToken $token)
217
    {
218 6
        $response = $this->fetchResourceOwnerDetails($token);
219
220 4
        // We are always getting an array. We have to check if it is
221
        // the array we created
222
        if (array_key_exists('jwt', $response)) {
223
            $response = $response['jwt'];
224
        }
225
226
        $response = $this->decryptResponse($response);
227
228
        return $this->createResourceOwner($response, $token);
0 ignored issues
show
Bug introduced by
It seems like $response defined by $this->decryptResponse($response) on line 226 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...
229
    }
230 4
231
    /**
232 4
     * Updates expected encryption algorithm of Keycloak instance.
233
     *
234 4
     * @param string  $encryptionAlgorithm
235
     *
236
     * @return Keycloak
237
     */
238
    public function setEncryptionAlgorithm($encryptionAlgorithm)
239
    {
240
        $this->encryptionAlgorithm = $encryptionAlgorithm;
241
242
        return $this;
243
    }
244 4
245
    /**
246 4
     * Updates expected encryption key of Keycloak instance.
247
     *
248 4
     * @param string  $encryptionKey
249
     *
250
     * @return Keycloak
251
     */
252
    public function setEncryptionKey($encryptionKey)
253
    {
254
        $this->encryptionKey = $encryptionKey;
255
256
        return $this;
257
    }
258
259 4
    /**
260
     * Updates expected encryption key of Keycloak instance to content of given
261
     * file path.
262 4
     *
263 2
     * @param string  $encryptionKeyPath
264
     *
265
     * @return Keycloak
266
     */
267 4
    public function setEncryptionKeyPath($encryptionKeyPath)
268
    {
269
        try {
270
            $this->encryptionKey = file_get_contents($encryptionKeyPath);
271
        } catch (Exception $e) {
272
            // Not sure how to handle this yet.
273
        }
274
275 4
        return $this;
276
    }
277 4
278
    /**
279
     * Checks if provider is configured to use encryption.
280
     *
281
     * @return bool
282
     */
283
    public function usesEncryption()
284
    {
285
        return (bool) $this->encryptionAlgorithm && $this->encryptionKey;
286
    }
287
288
    /**
289
     * Parses the response according to its content-type header.
290
     *
291
     * @throws UnexpectedValueException
292
     * @param  ResponseInterface $response
293
     * @return array
294
     */
295
    protected function parseResponse(ResponseInterface $response)
296
    {
297
        // We have a problem with keycloak when the userinfo responses
298
        // with a jwt token
299
        // Because it just return a jwt as string with the header
300
        // application/jwt
301
        // This can't be parsed to a array
302
        // Dont know why this function only allow an array as return value...
303
        $content = (string) $response->getBody();
304
        $type = $this->getContentType($response);
305
306
        if (strpos($type, 'jwt') !== false) {
307
            // Here we make the temporary array
308
            return ['jwt' => $content];
309
        }
310
311
        return parent::parseResponse($response);
312
    }
313
}
314