Completed
Push — master ( 6c2dd9...ca8e19 )
by Eric
03:03
created

Keycloak::decryptResponse()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 20
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 11
c 1
b 0
f 0
nc 3
nop 1
dl 0
loc 20
rs 9.9
1
<?php
2
3
namespace Makuro\Directus\KeycloakClient\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 Makuro\Directus\KeycloakClient\Provider\Exception\EncryptionConfigurationException;
12
use Psr\Http\Message\ResponseInterface;
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
    /**
51
     * Constructs an OAuth 2.0 service provider.
52
     *
53
     * @param array $options An array of options to set on this provider.
54
     *     Options include `clientId`, `clientSecret`, `redirectUri`, and `state`.
55
     *     Individual providers may introduce more options, as needed.
56
     * @param array $collaborators An array of collaborators that may be used to
57
     *     override this provider's default behavior. Collaborators include
58
     *     `grantFactory`, `requestFactory`, `httpClient`, and `randomFactory`.
59
     *     Individual providers may introduce more collaborators, as needed.
60
     */
61
    public function __construct(array $options = [], array $collaborators = [])
62
    {
63
        if (isset($options['encryptionKeyPath'])) {
64
            $this->setEncryptionKeyPath($options['encryptionKeyPath']);
65
            unset($options['encryptionKeyPath']);
66
        }
67
        parent::__construct($options, $collaborators);
68
    }
69
70
    /**
71
     * Attempts to decrypt the given response.
72
     *
73
     * @param string|array|null $response
74
     *
75
     * @return string|array|null
76
     * @throws EncryptionConfigurationException
77
     */
78
    public function decryptResponse($response)
79
    {
80
        if (!is_string($response)) {
81
            return $response;
82
        }
83
84
        if ($this->usesEncryption()) {
85
            return json_decode(
86
                json_encode(
87
                    JWT::decode(
88
                        $response,
89
                        $this->encryptionKey,
90
                        array($this->encryptionAlgorithm)
91
                    )
92
                ),
93
                true
94
            );
95
        }
96
97
        throw EncryptionConfigurationException::undeterminedEncryption();
98
    }
99
100
    /**
101
     * Get authorization url to begin OAuth flow
102
     *
103
     * @return string
104
     */
105
    public function getBaseAuthorizationUrl()
106
    {
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
     */
117
    public function getBaseAccessTokenUrl(array $params)
118
    {
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
     */
129
    public function getResourceOwnerDetailsUrl(AccessToken $token)
130
    {
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
     */
140
    public function getLogoutUrl(array $options = [])
141
    {
142
        $base = $this->getBaseLogoutUrl();
143
        $params = $this->getAuthorizationParameters($options);
144
        $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
     */
153
    private function getBaseLogoutUrl()
154
    {
155
        return $this->getBaseUrlWithRealm() . '/protocol/openid-connect/logout';
156
    }
157
158
    /**
159
     * Creates base url from provider configuration.
160
     *
161
     * @return string
162
     */
163
    protected function getBaseUrlWithRealm()
164
    {
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
     */
176
    protected function getDefaultScopes()
177
    {
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
     */
189
    protected function checkResponse(ResponseInterface $response, $data)
190
    {
191
        if (!empty($data['error'])) {
192
            $error = $data['error'].': '.$data['error_description'];
193
            throw new IdentityProviderException($error, 0, $data);
194
        }
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
     */
204
    protected function createResourceOwner(array $response, AccessToken $token)
205
    {
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
     *
214
     * @return KeycloakResourceOwner
215
     * @throws EncryptionConfigurationException
216
     */
217
    public function getResourceOwner(AccessToken $token)
218
    {
219
        $response = $this->fetchResourceOwnerDetails($token);
220
221
        $response = $this->decryptResponse($response);
222
223
        return $this->createResourceOwner($response, $token);
0 ignored issues
show
Bug introduced by
It seems like $response can also be of type null and string; however, parameter $response of Makuro\Directus\Keycloak...::createResourceOwner() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

223
        return $this->createResourceOwner(/** @scrutinizer ignore-type */ $response, $token);
Loading history...
224
    }
225
226
    /**
227
     * Updates expected encryption algorithm of Keycloak instance.
228
     *
229
     * @param string  $encryptionAlgorithm
230
     *
231
     * @return Keycloak
232
     */
233
    public function setEncryptionAlgorithm($encryptionAlgorithm)
234
    {
235
        $this->encryptionAlgorithm = $encryptionAlgorithm;
236
237
        return $this;
238
    }
239
240
    /**
241
     * Updates expected encryption key of Keycloak instance.
242
     *
243
     * @param string  $encryptionKey
244
     *
245
     * @return Keycloak
246
     */
247
    public function setEncryptionKey($encryptionKey)
248
    {
249
        $this->encryptionKey = $encryptionKey;
250
251
        return $this;
252
    }
253
254
    /**
255
     * Updates expected encryption key of Keycloak instance to content of given
256
     * file path.
257
     *
258
     * @param string  $encryptionKeyPath
259
     *
260
     * @return Keycloak
261
     */
262
    public function setEncryptionKeyPath(string $encryptionKeyPath)
263
    {
264
        try {
265
            $this->encryptionKey = file_get_contents($encryptionKeyPath);
266
        } catch (Exception $e) {
267
            // Not sure how to handle this yet.
268
        }
269
270
        return $this;
271
    }
272
273
    /**
274
     * Checks if provider is configured to use encryption.
275
     *
276
     * @return bool
277
     */
278
    public function usesEncryption()
279
    {
280
        return (bool) $this->encryptionAlgorithm && $this->encryptionKey;
281
    }
282
}
283