Completed
Push — master ( d411f2...388e0b )
by
unknown
11:13
created

Authenticator::authenticateByToken()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 28
rs 9.1608
c 0
b 0
f 0
cc 5
nc 5
nop 0
1
<?php
2
3
namespace Charcoal\User;
4
5
use InvalidArgumentException;
6
use RuntimeException;
7
8
// From PSR-3
9
use Psr\Log\LoggerAwareInterface;
10
use Psr\Log\LoggerAwareTrait;
11
12
// From 'charcoal-factory'
13
use Charcoal\Factory\FactoryInterface;
14
15
/**
16
 * The Authenticator service helps with user authentication / login.
17
 *
18
 * ## Constructor dependencies
19
 *
20
 * Constructor dependencies are passed as an array of `key=>value` pair.
21
 * The required dependencies are:
22
 *
23
 * - `logger` A PSR3 logger instance
24
 * - `user_type` The user object type (FQN or ident)
25
 * - `user_factory` The Factory used to instanciate new users.
26
 * - `token_type` The auth token object type (FQN or ident)
27
 * - `token_factory` The Factory used to instanciate new auth tokens.
28
 */
29
class Authenticator implements LoggerAwareInterface
30
{
31
    use LoggerAwareTrait;
32
33
    /**
34
     * The user object type.
35
     *
36
     * @var string
37
     */
38
    private $userType;
39
40
    /**
41
     * Store the user model factory instance for the current class.
42
     *
43
     * @var FactoryInterface
44
     */
45
    private $userFactory;
46
47
    /**
48
     * The auth-token object type.
49
     *
50
     * @var string
51
     */
52
    private $tokenType;
53
54
    /**
55
     * Store the auth-token model factory instance for the current class.
56
     *
57
     * @var FactoryInterface
58
     */
59
    private $tokenFactory;
60
61
    /**
62
     * @param array $data Class dependencies.
63
     */
64
    public function __construct(array $data)
65
    {
66
        $this->setLogger($data['logger']);
67
        $this->setUserType($data['user_type']);
68
        $this->setUserFactory($data['user_factory']);
69
        $this->setTokenType($data['token_type']);
70
        $this->setTokenFactory($data['token_factory']);
71
    }
72
73
    /**
74
     * Determine if the current user is authenticated.
75
     *
76
     * The user is authenticated via _session ID_ or _auth token_.
77
     *
78
     * @return \Charcoal\User\UserInterface|null Returns the authenticated user object
79
     *     or NULL if not authenticated.
80
     */
81
    public function authenticate()
82
    {
83
        $u = $this->authenticateBySession();
84
        if ($u) {
85
            return $u;
86
        }
87
88
        $u = $this->authenticateByToken();
89
        if ($u) {
90
            return $u;
91
        }
92
93
        return null;
94
    }
95
96
    /**
97
     * Attempt to authenticate a user using the given credentials.
98
     *
99
     * @param string $username Username, part of necessary credentials.
100
     * @param string $password Password, part of necessary credentials.
101
     * @param string|null $key Optional property (key) to use for the username. Defaults to actual object's key.
102
     * @throws InvalidArgumentException If username or password are invalid or empty.
103
     * @return \Charcoal\User\UserInterface|null Returns the authenticated user object
104
     *     or NULL if not authenticated.
105
     */
106
    public function authenticateByPassword($username, $password, $key=null)
107
    {
108
        if (!is_string($username) || !is_string($password)) {
109
            throw new InvalidArgumentException(
110
                'Username and password must be strings'
111
            );
112
        }
113
114
        if ($username == '' || $password == '') {
115
            throw new InvalidArgumentException(
116
                'Username and password can not be empty.'
117
            );
118
        }
119
120
        $u = $this->userFactory()->create($this->userType());
121
        if (!$u->source()->tableExists()) {
122
            $u->source()->createTable();
123
        }
124
125
        // Force lowercase
126
        $username = mb_strtolower($username);
127
128
        // Load the user by username
129
        if ($key === null) {
130
            $key = $u->key();
131
        }
132
        $u->loadFrom($key, $username);
133
134
        if ($u[$key] != $username) {
135
            return null;
136
        }
137
138
        if ($u->active() === false) {
139
            return null;
140
        }
141
142
        // Validate password
143
        if (password_verify($password, $u->password())) {
144
            if (password_needs_rehash($u->password(), PASSWORD_DEFAULT)) {
145
                $this->logger->notice(sprintf(
146
                    'Rehashing password for user "%s" (%s)',
147
                    $u->username(),
148
                    $this->userType()
149
                ));
150
                $hash = password_hash($password, PASSWORD_DEFAULT);
151
                $u->setPassword($hash);
152
                $u->update(['password']);
153
            }
154
155
            $u->login();
156
157
            return $u;
158
        } else {
159
            $this->logger->warning('Invalid login attempt for user: invalid password.');
160
161
            return null;
162
        }
163
    }
164
165
    /**
166
     * Retrieve the user object type.
167
     *
168
     * @return string
169
     */
170
    protected function userType()
171
    {
172
        return $this->userType;
173
    }
174
175
176
    /**
177
     * Retrieve the user model factory.
178
     *
179
     * @throws RuntimeException If the model factory was not previously set.
180
     * @return FactoryInterface
181
     */
182
    protected function userFactory()
183
    {
184
        return $this->userFactory;
185
    }
186
187
    /**
188
     * Retrieve the auth-token object type.
189
     *
190
     * @return string
191
     */
192
    protected function tokenType()
193
    {
194
        return $this->tokenType;
195
    }
196
197
    /**
198
     * Retrieve the auth-token model factory.
199
     *
200
     * @throws RuntimeException If the token factory was not previously set.
201
     * @return FactoryInterface
202
     */
203
    protected function tokenFactory()
204
    {
205
        return $this->tokenFactory;
206
    }
207
208
    /**
209
     * Set the user object type (model).
210
     *
211
     * @param string $type The user object type.
212
     * @throws InvalidArgumentException If the user object type parameter is not a string.
213
     * @return void
214
     */
215
    private function setUserType($type)
216
    {
217
        if (!is_string($type)) {
218
            throw new InvalidArgumentException(
219
                'User object type must be a string'
220
            );
221
        }
222
223
        $this->userType = $type;
224
    }
225
226
    /**
227
     * Set a user model factory.
228
     *
229
     * @param FactoryInterface $factory The factory used to create new user instances.
230
     * @return void
231
     */
232
    private function setUserFactory(FactoryInterface $factory)
233
    {
234
        $this->userFactory = $factory;
235
    }
236
237
    /**
238
     * Set the authorization token type (model).
239
     *
240
     * @param string $type The auth-token object type.
241
     * @throws InvalidArgumentException If the token object type parameter is not a string.
242
     * @return void
243
     */
244
    private function setTokenType($type)
245
    {
246
        if (!is_string($type)) {
247
            throw new InvalidArgumentException(
248
                'Token object type must be a string'
249
            );
250
        }
251
252
        $this->tokenType = $type;
253
    }
254
255
    /**
256
     * Set a model factory for token-based authentication.
257
     *
258
     * @param FactoryInterface $factory The factory used to create new auth-token instances.
259
     * @return void
260
     */
261
    private function setTokenFactory(FactoryInterface $factory)
262
    {
263
        $this->tokenFactory = $factory;
264
    }
265
266
    /**
267
     * Attempt to authenticate a user using their session ID.
268
     *
269
     * @return \Charcoal\User\UserInterface|null Returns the authenticated user object
270
     *     or NULL if not authenticated.
271
     */
272
    private function authenticateBySession()
273
    {
274
        $u = $this->userFactory()->create($this->userType());
275
        // Call static method on user
276
        $u = call_user_func([get_class($u), 'getAuthenticated'], $this->userFactory());
277
278
        if ($u && $u->id()) {
279
            $u->saveToSession();
280
281
            return $u;
282
        } else {
283
            return null;
284
        }
285
    }
286
287
    /**
288
     * Attempt to authenticate a user using their auth token.
289
     *
290
     * @return \Charcoal\User\UserInterface|null Returns the authenticated user object
291
     *     or NULL if not authenticated.
292
     */
293
    private function authenticateByToken()
294
    {
295
        $tokenType = $this->tokenType();
296
        $authToken = $this->tokenFactory()->create($tokenType);
297
298
        if ($authToken->metadata()->enabled() !== true) {
299
            return null;
300
        }
301
302
        $tokenData = $authToken->getTokenDataFromCookie();
303
        if (!$tokenData) {
304
            return null;
305
        }
306
        $username = $authToken->getUsernameFromToken($tokenData['ident'], $tokenData['token']);
307
        if (!$username) {
308
            return null;
309
        }
310
311
        $u = $this->userFactory()->create($this->userType());
312
        $u->load($username);
313
314
        if ($u->id()) {
315
            $u->saveToSession();
316
            return $u;
317
        } else {
318
            return null;
319
        }
320
    }
321
}
322