Passed
Pull Request — master (#26)
by Jitendra
01:50
created

ApiAuthenticator   A

Complexity

Total Complexity 16

Size/Duplication

Total Lines 144
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 16
dl 0
loc 144
rs 10
c 0
b 0
f 0

10 Methods

Rating   Name   Duplication   Size   Complexity  
A bySubject() 0 5 1
A findUser() 0 16 4
A mustBeAuthenticated() 0 4 2
A configure() 0 3 1
A getSubject() 0 5 1
A createRefreshToken() 0 18 1
A __construct() 0 3 1
A byRefreshToken() 0 13 3
A getScopes() 0 5 1
A byCredential() 0 5 1
1
<?php
2
3
namespace PhalconExt\Factory;
4
5
use Phalcon\Db\Adapter as Db;
0 ignored issues
show
Bug introduced by
The type Phalcon\Db\Adapter 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 Phalcon\Security\Random;
0 ignored issues
show
Bug introduced by
The type Phalcon\Security\Random 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 PhalconExt\Contract\ApiAuthenticator as Contract;
8
9
/**
10
 * Factory implementation for ApiAuthenticator. You might want to roll out your own if it doesnt suffice.
11
 *
12
 * Uses `users` table with `id`, `username`, `password`, `scopes`, `created_at` fields.
13
 * And `tokens` table with `id`, `user_id`, `type`, `token`, `created_at`, `expire_at` fields.
14
 *
15
 * You can access the current authenticator application wide as: `$di->get('authenticator')`.
16
 */
17
class ApiAuthenticator implements Contract
18
{
19
    /** @var Db */
20
    protected $db;
21
22
    /** @var array User data */
23
    protected $user = [];
24
25
    /** @var array Config options */
26
    protected $config = [];
27
28
    public function __construct(Db $db)
29
    {
30
        $this->db = $db;
31
    }
32
33
    /**
34
     * {@inheritdoc}
35
     */
36
    public function configure(array $config)
37
    {
38
        $this->config = $config;
39
    }
40
41
    /**
42
     * {@inheritdoc}
43
     */
44
    public function byCredential(string $username, string $password): bool
45
    {
46
        $this->user = $this->findUser('username', $username, $password);
47
48
        return !empty($this->user);
49
    }
50
51
    /**
52
     * Find user based on given property and value. If password is provided it it validated.
53
     *
54
     * @param string      $column
55
     * @param int|string  $value
56
     * @param null|string $password
57
     *
58
     * @return array User detail, empty on failure.
59
     */
60
    protected function findUser(string $column, $value, $password = null): array
61
    {
62
        $user = $this->db->fetchOne(
63
            "SELECT * FROM users WHERE $column = ?",
64
            \PDO::FETCH_ASSOC,
65
            [$value]
66
        );
67
68
        $hash = $user['password'] ?? null;
69
        if ($password && !\password_verify($password, $hash)) {
70
            return [];
71
        }
72
73
        unset($user['password']);
74
75
        return $user ?: [];
76
    }
77
78
    /**
79
     * {@inheritdoc}
80
     */
81
    public function byRefreshToken(string $refreshToken): bool
82
    {
83
        $token = $this->db->fetchOne(
84
            'SELECT * FROM tokens WHERE type = ? AND token = ?',
85
            \PDO::FETCH_ASSOC,
86
            ['refresh', $refreshToken]
87
        );
88
89
        if (!$token || new \DateTime > new \DateTime($token['expire_at'])) {
90
            return false;
91
        }
92
93
        return $this->bySubject($token['user_id']);
94
    }
95
96
    /**
97
     * {@inheritdoc}
98
     */
99
    public function bySubject(int $subject): bool
100
    {
101
        $this->user = $this->findUser('id', $subject);
102
103
        return !empty($this->user);
104
    }
105
106
    /**
107
     * {@inheritdoc}
108
     */
109
    public function createRefreshToken(): string
110
    {
111
        $this->mustBeAuthenticated();
112
113
        $tokenLen     = \min($this->config['tokenLength'] ?? 0, 32);
114
        $prefix       = \substr($this->config['tokenPrefix'] ?? '', 0, 4);
115
        $random       = (new Random)->bytes($tokenLen - \strlen($prefix));
116
        $refreshToken = $prefix . \bin2hex($random);
117
118
        $this->db->insertAsDict('tokens', [
119
            'type'       => 'refresh',
120
            'token'      => $refreshToken,
121
            'user_id'    => $this->user['id'],
122
            'created_at' => \date('Y-m-d H:i:s'),
123
            'expire_at'  => \date('Y-m-d H:i:s', \time() + $this->config['refreshMaxAge']),
124
        ]);
125
126
        return $refreshToken;
127
    }
128
129
    /**
130
     * Throw up if user is not authenticated yet.
131
     *
132
     * @codeCoverageIgnore
133
     *
134
     * @throws \LogicException
135
     */
136
    protected function mustBeAuthenticated()
137
    {
138
        if (!$this->user) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->user of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
139
            throw new \LogicException('You must authenticate an user first');
140
        }
141
    }
142
143
    /**
144
     * {@inheritdoc}
145
     */
146
    public function getSubject(): int
147
    {
148
        $this->mustBeAuthenticated();
149
150
        return $this->user['id'];
151
    }
152
153
    /**
154
     * {@inheritdoc}
155
     */
156
    public function getScopes(): array
157
    {
158
        $this->mustBeAuthenticated();
159
160
        return \explode(',', $this->user['scopes']);
161
    }
162
}
163