ApiAuthenticator::configure()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 2
Metric Value
eloc 1
c 2
b 0
f 2
dl 0
loc 3
rs 10
cc 1
nc 1
nop 1
1
<?php
2
3
/*
4
 * This file is part of the PHALCON-EXT package.
5
 *
6
 * (c) Jitendra Adhikari <[email protected]>
7
 *     <https://github.com/adhocore>
8
 *
9
 * Licensed under MIT license.
10
 */
11
12
namespace PhalconExt\Factory;
13
14
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...
15
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...
16
use PhalconExt\Contract\ApiAuthenticator as Contract;
17
18
/**
19
 * Factory implementation for ApiAuthenticator. You might want to roll out your own if it doesnt suffice.
20
 *
21
 * Uses `users` table with `id`, `username`, `password`, `scopes`, `created_at` fields.
22
 * And `tokens` table with `id`, `user_id`, `type`, `token`, `created_at`, `expire_at` fields.
23
 *
24
 * You can access the current authenticator application wide as: `$di->get('authenticator')`.
25
 */
26
class ApiAuthenticator implements Contract
27
{
28
    /** @var Db */
29
    protected $db;
30
31
    /** @var array User data */
32
    protected $user = [];
33
34
    /** @var array Config options */
35
    protected $config = [];
36
37
    public function __construct(Db $db)
38
    {
39
        $this->db = $db;
40
    }
41
42
    /**
43
     * {@inheritdoc}
44
     */
45
    public function configure(array $config)
46
    {
47
        $this->config = $config;
48
    }
49
50
    /**
51
     * {@inheritdoc}
52
     */
53
    public function byCredential(string $username, string $password): bool
54
    {
55
        $this->user = $this->findUser('username', $username, $password);
56
57
        return !empty($this->user);
58
    }
59
60
    /**
61
     * Find user based on given property and value. If password is provided it it validated.
62
     *
63
     * @param string      $column
64
     * @param int|string  $value
65
     * @param null|string $password
66
     *
67
     * @return array User detail, empty on failure.
68
     */
69
    protected function findUser(string $column, $value, $password = null): array
70
    {
71
        $user = $this->db->fetchOne(
72
            "SELECT * FROM users WHERE $column = ?",
73
            \PDO::FETCH_ASSOC,
74
            [$value]
75
        );
76
77
        $hash = $user['password'] ?? null;
78
        if ($password && !\password_verify($password, $hash)) {
0 ignored issues
show
Bug introduced by
It seems like $hash can also be of type null; however, parameter $hash of password_verify() does only seem to accept string, 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

78
        if ($password && !\password_verify($password, /** @scrutinizer ignore-type */ $hash)) {
Loading history...
79
            return [];
80
        }
81
82
        unset($user['password']);
83
84
        return $user ?: [];
85
    }
86
87
    /**
88
     * {@inheritdoc}
89
     */
90
    public function byRefreshToken(string $refreshToken): bool
91
    {
92
        $token = $this->db->fetchOne(
93
            'SELECT * FROM tokens WHERE type = ? AND token = ?',
94
            \PDO::FETCH_ASSOC,
95
            ['refresh', $refreshToken]
96
        );
97
98
        if (!$token || new \DateTime > new \DateTime($token['expire_at'])) {
99
            return false;
100
        }
101
102
        return $this->bySubject($token['user_id']);
103
    }
104
105
    /**
106
     * {@inheritdoc}
107
     */
108
    public function bySubject(int $subject): bool
109
    {
110
        $this->user = $this->findUser('id', $subject);
111
112
        return !empty($this->user);
113
    }
114
115
    /**
116
     * {@inheritdoc}
117
     */
118
    public function createRefreshToken(): string
119
    {
120
        $this->mustBeAuthenticated();
121
122
        $tokenLen     = \min($this->config['tokenLength'] ?? 0, 32);
123
        $prefix       = \substr($this->config['tokenPrefix'] ?? '', 0, 4);
124
        $random       = (new Random)->bytes($tokenLen - \strlen($prefix));
125
        $refreshToken = $prefix . \bin2hex($random);
126
127
        $this->db->insertAsDict('tokens', [
128
            'type'       => 'refresh',
129
            'token'      => $refreshToken,
130
            'user_id'    => $this->user['id'],
131
            'created_at' => \date('Y-m-d H:i:s'),
132
            'expire_at'  => \date('Y-m-d H:i:s', \time() + $this->config['refreshMaxAge']),
133
        ]);
134
135
        return $refreshToken;
136
    }
137
138
    /**
139
     * Throw up if user is not authenticated yet.
140
     *
141
     * @codeCoverageIgnore
142
     *
143
     * @throws \LogicException
144
     */
145
    protected function mustBeAuthenticated()
146
    {
147
        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...
148
            throw new \LogicException('You must authenticate an user first');
149
        }
150
    }
151
152
    /**
153
     * {@inheritdoc}
154
     */
155
    public function getSubject(): int
156
    {
157
        $this->mustBeAuthenticated();
158
159
        return $this->user['id'];
160
    }
161
162
    /**
163
     * {@inheritdoc}
164
     */
165
    public function getScopes(): array
166
    {
167
        $this->mustBeAuthenticated();
168
169
        return \explode(',', $this->user['scopes']);
170
    }
171
}
172