Completed
Pull Request — develop (#816)
by Kristijan
07:28
created

Cassandra::expireValue()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 13
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 7
nc 2
nop 1
1
<?php
2
3
namespace OAuth2\Storage;
4
5
use phpcassa\ColumnFamily;
0 ignored issues
show
Bug introduced by
The type phpcassa\ColumnFamily was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
6
use phpcassa\ColumnSlice;
0 ignored issues
show
Bug introduced by
The type phpcassa\ColumnSlice was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
7
use phpcassa\Connection\ConnectionPool;
0 ignored issues
show
Bug introduced by
The type phpcassa\Connection\ConnectionPool was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
8
use OAuth2\OpenID\Storage\UserClaimsInterface;
9
use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface;
10
11
/**
12
 * Cassandra storage for all storage types
13
 *
14
 * To use, install "thobbs/phpcassa" via composer
15
 * <code>
16
 *  composer require thobbs/phpcassa:dev-master
17
 * </code>
18
 *
19
 * Once this is done, instantiate the
20
 * <code>
21
 *  $cassandra = new \phpcassa\Connection\ConnectionPool('oauth2_server', array('127.0.0.1:9160'));
22
 * </code>
23
 *
24
 * Then, register the storage client:
25
 * <code>
26
 *  $storage = new OAuth2\Storage\Cassandra($cassandra);
27
 *  $storage->setClientDetails($client_id, $client_secret, $redirect_uri);
28
 * </code>
29
 *
30
 * @see test/lib/OAuth2/Storage/Bootstrap::getCassandraStorage
31
 */
32
class Cassandra implements AuthorizationCodeInterface,
33
    AccessTokenInterface,
34
    ClientCredentialsInterface,
35
    UserCredentialsInterface,
36
    RefreshTokenInterface,
37
    JwtBearerInterface,
38
    ScopeInterface,
39
    PublicKeyInterface,
40
    UserClaimsInterface,
41
    OpenIDAuthorizationCodeInterface
42
{
43
44
    private $cache;
45
46
    /* The cassandra client */
47
    protected $cassandra;
48
49
    /* Configuration array */
50
    protected $config;
51
52
    /**
53
     * Cassandra Storage! uses phpCassa
54
     *
55
     * @param \phpcassa\ConnectionPool $cassandra
0 ignored issues
show
Bug introduced by
The type phpcassa\ConnectionPool was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
56
     * @param array                    $config
57
     */
58
    public function __construct($connection = array(), array $config = array())
59
    {
60
        if ($connection instanceof ConnectionPool) {
61
            $this->cassandra = $connection;
62
        } else {
63
            if (!is_array($connection)) {
64
                throw new \InvalidArgumentException('First argument to OAuth2\Storage\Cassandra must be an instance of phpcassa\Connection\ConnectionPool or a configuration array');
65
            }
66
            $connection = array_merge(array(
67
                'keyspace' => 'oauth2',
68
                'servers'  => null,
69
            ), $connection);
70
71
            $this->cassandra = new ConnectionPool($connection['keyspace'], $connection['servers']);
72
        }
73
74
        $this->config = array_merge(array(
75
            // cassandra config
76
            'column_family' => 'auth',
77
78
            // key names
79
            'client_key' => 'oauth_clients:',
80
            'access_token_key' => 'oauth_access_tokens:',
81
            'refresh_token_key' => 'oauth_refresh_tokens:',
82
            'code_key' => 'oauth_authorization_codes:',
83
            'user_key' => 'oauth_users:',
84
            'jwt_key' => 'oauth_jwt:',
85
            'scope_key' => 'oauth_scopes:',
86
            'public_key_key'  => 'oauth_public_keys:',
87
        ), $config);
88
    }
89
90
    protected function getValue($key)
91
    {
92
        if (isset($this->cache[$key])) {
93
            return $this->cache[$key];
94
        }
95
        $cf = new ColumnFamily($this->cassandra, $this->config['column_family']);
96
97
        try {
98
            $value = $cf->get($key, new ColumnSlice("", ""));
99
            $value = array_shift($value);
100
        } catch (\cassandra\NotFoundException $e) {
0 ignored issues
show
Bug introduced by
The type cassandra\NotFoundException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
101
            return false;
102
        }
103
104
        return json_decode($value, true);
105
    }
106
107
    protected function setValue($key, $value, $expire = 0)
108
    {
109
        $this->cache[$key] = $value;
110
111
        $cf = new ColumnFamily($this->cassandra, $this->config['column_family']);
112
113
        $str = json_encode($value);
114
        if ($expire > 0) {
115
            try {
116
                $seconds = $expire - time();
117
                // __data key set as C* requires a field, note: max TTL can only be 630720000 seconds
118
                $cf->insert($key, array('__data' => $str), null, $seconds);
119
            } catch (\Exception $e) {
120
                return false;
121
            }
122
        } else {
123
            try {
124
                // __data key set as C* requires a field
125
                $cf->insert($key, array('__data' => $str));
126
            } catch (\Exception $e) {
127
                return false;
128
            }
129
        }
130
131
        return true;
132
    }
133
134
    protected function expireValue($key)
135
    {
136
        unset($this->cache[$key]);
137
138
        $cf = new ColumnFamily($this->cassandra, $this->config['column_family']);
139
        try {
140
            // __data key set as C* requires a field
141
            $cf->remove($key, array('__data'));
142
        } catch (\Exception $e) {
143
            return false;
144
        }
145
146
        return true;
147
    }
148
149
    /* AuthorizationCodeInterface */
150
    public function getAuthorizationCode($code)
151
    {
152
        return $this->getValue($this->config['code_key'] . $code);
153
    }
154
155
    public function setAuthorizationCode($authorization_code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null)
156
    {
157
        return $this->setValue(
158
            $this->config['code_key'] . $authorization_code,
159
            compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token'),
160
            $expires
161
        );
162
    }
163
164
    public function expireAuthorizationCode($code)
165
    {
166
        $key = $this->config['code_key'] . $code;
167
        unset($this->cache[$key]);
168
169
        return $this->expireValue($key);
170
    }
171
172
    /* UserCredentialsInterface */
173
    public function checkUserCredentials($username, $password)
174
    {
175
        if ($user = $this->getUser($username)) {
176
            return $this->checkPassword($user, $password);
177
        }
178
179
        return false;
180
    }
181
182
    // plaintext passwords are bad!  Override this for your application
183
    protected function checkPassword($user, $password)
184
    {
185
        return $user['password'] == sha1($password);
186
    }
187
188
    public function getUserDetails($username)
189
    {
190
        return $this->getUser($username);
191
    }
192
193
    public function getUser($username)
194
    {
195
        if (!$userInfo = $this->getValue($this->config['user_key'] . $username)) {
196
            return false;
197
        }
198
199
        // the default behavior is to use "username" as the user_id
200
        return array_merge(array(
201
            'user_id' => $username,
202
        ), $userInfo);
203
    }
204
205
    public function setUser($username, $password, $first_name = null, $last_name = null)
206
    {
207
        $password = sha1($password);
208
209
        return $this->setValue(
210
            $this->config['user_key'] . $username,
211
            compact('username', 'password', 'first_name', 'last_name')
212
        );
213
    }
214
215
    /* ClientCredentialsInterface */
216
    public function checkClientCredentials($client_id, $client_secret = null)
217
    {
218
        if (!$client = $this->getClientDetails($client_id)) {
219
            return false;
220
        }
221
222
        return isset($client['client_secret'])
223
            && $client['client_secret'] == $client_secret;
224
    }
225
226
    public function isPublicClient($client_id)
227
    {
228
        if (!$client = $this->getClientDetails($client_id)) {
0 ignored issues
show
Unused Code introduced by
The assignment to $client is dead and can be removed.
Loading history...
229
            return false;
230
        }
231
232
        return empty($result['client_secret']);;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $result seems to never exist and therefore empty should always be true.
Loading history...
233
    }
234
235
    /* ClientInterface */
236
    public function getClientDetails($client_id)
237
    {
238
        return $this->getValue($this->config['client_key'] . $client_id);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getValue($...ent_key'] . $client_id) also could return the type false which is incompatible with the return type mandated by OAuth2\Storage\ClientInterface::getClientDetails() of array.
Loading history...
239
    }
240
241
    public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null)
242
    {
243
        return $this->setValue(
244
            $this->config['client_key'] . $client_id,
245
            compact('client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'user_id')
246
        );
247
    }
248
249
    public function checkRestrictedGrantType($client_id, $grant_type)
250
    {
251
        $details = $this->getClientDetails($client_id);
252
        if (isset($details['grant_types'])) {
253
            $grant_types = explode(' ', $details['grant_types']);
254
255
            return in_array($grant_type, (array) $grant_types);
256
        }
257
258
        // if grant_types are not defined, then none are restricted
259
        return true;
260
    }
261
262
    /* RefreshTokenInterface */
263
    public function getRefreshToken($refresh_token)
264
    {
265
        return $this->getValue($this->config['refresh_token_key'] . $refresh_token);
266
    }
267
268
    public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null)
269
    {
270
        return $this->setValue(
271
            $this->config['refresh_token_key'] . $refresh_token,
272
            compact('refresh_token', 'client_id', 'user_id', 'expires', 'scope'),
273
            $expires
274
        );
275
    }
276
277
    public function unsetRefreshToken($refresh_token)
278
    {
279
        return $this->expireValue($this->config['refresh_token_key'] . $refresh_token);
280
    }
281
282
    /* AccessTokenInterface */
283
    public function getAccessToken($access_token)
284
    {
285
        return $this->getValue($this->config['access_token_key'].$access_token);
286
    }
287
288
    public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null)
289
    {
290
        return $this->setValue(
291
            $this->config['access_token_key'].$access_token,
292
            compact('access_token', 'client_id', 'user_id', 'expires', 'scope'),
293
            $expires
294
        );
295
    }
296
297
    public function unsetAccessToken($access_token)
298
    {
299
        return $this->expireValue($this->config['access_token_key'] . $access_token);
300
    }
301
302
    /* ScopeInterface */
303
    public function scopeExists($scope)
304
    {
305
        $scope = explode(' ', $scope);
306
307
        $result = $this->getValue($this->config['scope_key'].'supported:global');
308
309
        $supportedScope = explode(' ', (string) $result);
310
311
        return (count(array_diff($scope, $supportedScope)) == 0);
312
    }
313
314
    public function getDefaultScope($client_id = null)
315
    {
316
        if (is_null($client_id) || !$result = $this->getValue($this->config['scope_key'].'default:'.$client_id)) {
317
            $result = $this->getValue($this->config['scope_key'].'default:global');
318
        }
319
320
        return $result;
321
    }
322
323
    public function setScope($scope, $client_id = null, $type = 'supported')
324
    {
325
        if (!in_array($type, array('default', 'supported'))) {
326
            throw new \InvalidArgumentException('"$type" must be one of "default", "supported"');
327
        }
328
329
        if (is_null($client_id)) {
330
            $key = $this->config['scope_key'].$type.':global';
331
        } else {
332
            $key = $this->config['scope_key'].$type.':'.$client_id;
333
        }
334
335
        return $this->setValue($key, $scope);
336
    }
337
338
    /*JWTBearerInterface */
339
    public function getClientKey($client_id, $subject)
340
    {
341
        if (!$jwt = $this->getValue($this->config['jwt_key'] . $client_id)) {
342
            return false;
343
        }
344
345
        if (isset($jwt['subject']) && $jwt['subject'] == $subject ) {
346
            return $jwt['key'];
347
        }
348
349
        return null;
350
    }
351
352
    public function setClientKey($client_id, $key, $subject = null)
353
    {
354
        return $this->setValue($this->config['jwt_key'] . $client_id, array(
355
            'key' => $key,
356
            'subject' => $subject
357
        ));
358
    }
359
360
    /*ScopeInterface */
361
    public function getClientScope($client_id)
362
    {
363
        if (!$clientDetails = $this->getClientDetails($client_id)) {
364
            return false;
365
        }
366
367
        if (isset($clientDetails['scope'])) {
368
            return $clientDetails['scope'];
369
        }
370
371
        return null;
372
    }
373
374
    public function getJti($client_id, $subject, $audience, $expiration, $jti)
375
    {
376
        //TODO: Needs cassandra implementation.
377
        throw new \Exception('getJti() for the Cassandra driver is currently unimplemented.');
378
    }
379
380
    public function setJti($client_id, $subject, $audience, $expiration, $jti)
381
    {
382
        //TODO: Needs cassandra implementation.
383
        throw new \Exception('setJti() for the Cassandra driver is currently unimplemented.');
384
    }
385
386
    /* PublicKeyInterface */
387
    public function getPublicKey($client_id = '')
388
    {
389
        $public_key = $this->getValue($this->config['public_key_key'] . $client_id);
390
        if (is_array($public_key)) {
391
            return $public_key['public_key'];
392
        }
393
        $public_key = $this->getValue($this->config['public_key_key']);
394
        if (is_array($public_key)) {
395
            return $public_key['public_key'];
396
        }
397
    }
398
399
    public function getPrivateKey($client_id = '')
400
    {
401
        $public_key = $this->getValue($this->config['public_key_key'] . $client_id);
402
        if (is_array($public_key)) {
403
            return $public_key['private_key'];
404
        }
405
        $public_key = $this->getValue($this->config['public_key_key']);
406
        if (is_array($public_key)) {
407
            return $public_key['private_key'];
408
        }
409
    }
410
411
    public function getEncryptionAlgorithm($client_id = null)
412
    {
413
        $public_key = $this->getValue($this->config['public_key_key'] . $client_id);
414
        if (is_array($public_key)) {
415
            return $public_key['encryption_algorithm'];
416
        }
417
        $public_key = $this->getValue($this->config['public_key_key']);
418
        if (is_array($public_key)) {
419
            return $public_key['encryption_algorithm'];
420
        }
421
422
        return 'RS256';
423
    }
424
425
    /* UserClaimsInterface */
426
    public function getUserClaims($user_id, $claims)
427
    {
428
        $userDetails = $this->getUserDetails($user_id);
429
        if (!is_array($userDetails)) {
430
            return false;
431
        }
432
433
        $claims = explode(' ', trim($claims));
434
        $userClaims = array();
435
436
        // for each requested claim, if the user has the claim, set it in the response
437
        $validClaims = explode(' ', self::VALID_CLAIMS);
438
        foreach ($validClaims as $validClaim) {
439
            if (in_array($validClaim, $claims)) {
440
                if ($validClaim == 'address') {
441
                    // address is an object with subfields
442
                    $userClaims['address'] = $this->getUserClaim($validClaim, $userDetails['address'] ?: $userDetails);
443
                } else {
444
                    $userClaims = array_merge($userClaims, $this->getUserClaim($validClaim, $userDetails));
445
                }
446
            }
447
        }
448
449
        return $userClaims;
450
    }
451
452
    protected function getUserClaim($claim, $userDetails)
453
    {
454
        $userClaims = array();
455
        $claimValuesString = constant(sprintf('self::%s_CLAIM_VALUES', strtoupper($claim)));
456
        $claimValues = explode(' ', $claimValuesString);
457
458
        foreach ($claimValues as $value) {
459
            if ($value == 'email_verified') {
460
                $userClaims[$value] = $userDetails[$value]=='true' ? true : false;
461
            } else {
462
                $userClaims[$value] = isset($userDetails[$value]) ? $userDetails[$value] : null;
463
            }
464
        }
465
466
        return $userClaims;
467
    }
468
469
}
470