Completed
Push — master ( dfec5f...1a2fe9 )
by Mārtiņš
02:17
created

Identification::updateStandardIdentity()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 5
ccs 0
cts 4
cp 0
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 1
crap 2
1
<?php
2
3
namespace Palladium\Service;
4
5
/**
6
 * Retrieval and handling of identities for registered users
7
 */
8
9
use Palladium\Entity as Entity;
10
use Palladium\Exception\PasswordMismatch;
11
use Palladium\Exception\KeyMismatch;
12
use Palladium\Exception\CompromisedCookie;
13
use Palladium\Exception\IdentityExpired;
14
use Palladium\Repository\Identity as Repository;
15
use Psr\Log\LoggerInterface;
16
17
class Identification
18
{
19
20
    const DEFAULT_COOKIE_LIFESPAN = 14400; // 4 hours
21
    const DEFAULT_TOKEN_LIFESPAN = 28800; // 8 hours
22
    const DEFAULT_HASH_COST = 12;
23
24
    private $repository;
25
    private $logger;
26
27
    private $cookieLifespan;
28
    private $hashCost;
29
30
    /**
31
     * @param Palladium\Repository\Identity $repository Repository for abstracting persistence layer structures
32
     * @param Psr\Log\LoggerInterface $logger PSR-3 compatible logger
33
     * @param int $cookieLifespan Lifespan of the authentication cookie in seconds (default: 4 hours)
34
     * @param int $hashCost Cost of the bcrypt hashing function (default: 12)
35
     */
36 14 View Code Duplication
    public function __construct(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
37
        Repository $repository,
38
        LoggerInterface $logger,
39
        int $cookieLifespan = self::DEFAULT_COOKIE_LIFESPAN,
40
        int $hashCost = self::DEFAULT_HASH_COST
41
        )
42
    {
43 14
        $this->repository = $repository;
44 14
        $this->logger = $logger;
45 14
        $this->cookieLifespan = $cookieLifespan;
46 14
        $this->hashCost = $hashCost;
47 14
    }
48
49
50 3
    public function loginWithPassword(Entity\StandardIdentity $identity, string $password): Entity\CookieIdentity
51
    {
52 3
        if ($identity->matchPassword($password) === false) {
53 1
            $this->logWrongPasswordNotice($identity, [
54 1
                'identifier' => $identity->getIdentifier(),
55 1
                'key' => $password,
56
                // this is the wrong password, if you store it in plain-text
57
                // then it becomes your responsibility
58
            ]);
59
60 1
            throw new PasswordMismatch;
61
        }
62
63 2
        $identity->setPassword($password);
64 2
        $this->updateStandardIdentityOnUse($identity);
65 2
        $cookie = $this->createCookieIdentity($identity);
66
67 2
        $this->logger->info('login successful', [
68
            'input' => [
69 2
                'identifier' => $identity->getIdentifier(),
70
            ],
71
            'user' => [
72 2
                'account' => $identity->getAccountId(),
73 2
                'identity' => $identity->getId(),
74
            ],
75
        ]);
76
77 2
        return $cookie;
78
    }
79
80
81 2
    private function updateStandardIdentityOnUse(Entity\StandardIdentity $identity)
82
    {
83 2
        $type = Entity\Identity::class;
84
85 2
        if ($identity->hasOldHash($this->hashCost)) {
86 1
            $identity->rehashPassword($this->hashCost);
87 1
            $type = Entity\StandardIdentity::class;
88
        }
89
90 2
        $identity->setLastUsed(time());
91 2
        $this->repository->save($identity, $type);
92 2
    }
93
94
95 3
    private function createCookieIdentity(Entity\Identity $identity): Entity\CookieIdentity
96
    {
97 3
        $cookie = new Entity\CookieIdentity;
98
99 3
        $cookie->setAccountId($identity->getAccountId());
100 3
        $cookie->generateNewSeries();
101
102 3
        $cookie->generateNewKey();
103 3
        $cookie->setStatus(Entity\Identity::STATUS_ACTIVE);
104 3
        $cookie->setExpiresOn(time() + $this->cookieLifespan);
105
106
107 3
        $parentId = $identity->getParentId();
108
109 3
        if (null === $parentId) {
110 3
            $parentId = $identity->getId();
111
        }
112
113 3
        $cookie->setParentId($parentId);
114 3
        $this->repository->save($cookie);
115
116 3
        return $cookie;
117
    }
118
119
120
    /**
121
     * @param string @key
122
     *
123
     * @throws \Palladium\Exception\CompromisedCookie if key does not match
124
     * @throws \Palladium\Exception\IdentityExpired if cookie is too old
125
     */
126 3
    public function loginWithCookie(Entity\CookieIdentity $identity, $key): Entity\CookieIdentity
127
    {
128 3
        $this->checkIdentityExpireTime($identity, $this->assembleCookieLogDetails($identity));
129 2
        $this->checkCookieKey($identity, $key);
130
131 1
        $identity->generateNewKey();
132 1
        $identity->setLastUsed(time());
133 1
        $identity->setExpiresOn(time() + $this->cookieLifespan);
134
135 1
        $this->repository->save($identity);
136
137 1
        $this->logExpectedBehaviour($identity, 'cookie updated');
138
139 1
        return $identity;
140
    }
141
142
143
    /**
144
     * @param string $key
145
     */
146 1
    public function logout(Entity\CookieIdentity $identity, $key)
147
    {
148 1
        $this->checkIdentityExpireTime($identity, $this->assembleCookieLogDetails($identity));
149 1
        $this->checkCookieKey($identity, $key);
150
151 1
        $this->changeIdentityStatus($identity, Entity\Identity::STATUS_DISCARDED);
152 1
        $this->logExpectedBehaviour($identity, 'logout successful');
153 1
    }
154
155
156 7
    private function checkIdentityExpireTime(Entity\Identity $identity, $details)
157
    {
158 7
        if ($identity->getExpiresOn() > time()) {
159 5
            return;
160
        }
161
162 2
        $this->logger->info('identity expired', $details);
163 2
        $this->changeIdentityStatus($identity, Entity\Identity::STATUS_EXPIRED);
164
165 2
        throw new IdentityExpired;
166
    }
167
168
169 5
    private function changeIdentityStatus(Entity\Identity $identity, int $status)
170
    {
171 5
        $identity->setStatus($status);
172 5
        $identity->setLastUsed(time());
173 5
        $this->repository->save($identity, Entity\Identity::class);
174 5
    }
175
176
177
    /**
178
     * Verify that the cookie based identity matches the key and,
179
     * if verification is failed, disable this given identity
180
     *
181
     * @param string $key
182
     * @throws \Palladium\Exception\CompromisedCookie if key does not match
183
     */
184 3
    private function checkCookieKey(Entity\CookieIdentity $identity, $key)
185
    {
186 3
        if ($identity->matchKey($key) === true) {
187 2
            return;
188
        }
189
190 1
        $this->changeIdentityStatus($identity, Entity\Identity::STATUS_BLOCKED);
191 1
        $this->logger->warning('compromised cookie', $this->assembleCookieLogDetails($identity));
192
193 1
        throw new CompromisedCookie;
194
    }
195
196
197 4 View Code Duplication
    private function assembleCookieLogDetails(Entity\CookieIdentity $identity): array
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
198
    {
199
        return [
200
            'input' => [
201 4
                'account' => $identity->getAccountId(),
202 4
                'series' => $identity->getSeries(),
203 4
                'key' => $identity->getKey(),
204
            ],
205
            'user' => [
206 4
                'account' => $identity->getAccountId(),
207 4
                'identity' => $identity->getId(),
208
            ],
209
        ];
210
    }
211
212
213 1
    public function discardIdentityCollection(Entity\IdentityCollection $list)
214
    {
215 1
        foreach ($list as $identity) {
216 1
            $identity->setStatus(Entity\Identity::STATUS_DISCARDED);
217
        }
218
219 1
        $this->repository->save($list);
220 1
    }
221
222
223 1
    public function blockIdentity(Entity\Identity $identity)
224
    {
225 1
        $identity->setStatus(Entity\Identity::STATUS_BLOCKED);
226 1
        $this->repository->save($identity, Entity\Identity::class);
227 1
    }
228
229
230
    /**
231
     * @codeCoverageIgnore
232
     */
233
    public function deleteIdentity(Entity\Identity $identity)
234
    {
235
        $this->repository->delete($identity, Entity\Identity::class);
236
    }
237
238
239
    /**
240
     * @param string $oldPassword
241
     * @param string $newPassword
242
     */
243 2
    public function changePassword(Entity\StandardIdentity $identity, $oldPassword, $newPassword)
244
    {
245
246 2
        if ($identity->matchPassword($oldPassword) === false) {
247 1
            $this->logWrongPasswordNotice($identity, [
248 1
                'account' => $identity->getAccountId(),
249 1
                'old-key' => $oldPassword, // the wrong password
250 1
                'new-key' => $newPassword,
251
            ]);
252
253 1
            throw new PasswordMismatch;
254
        }
255
256 1
        $identity->setPassword($newPassword);
257 1
        $this->repository->save($identity);
258
259 1
        $this->logExpectedBehaviour($identity, 'password changed');
260 1
    }
261
262
263
    /**
264
     * @param array $input
265
     */
266 2
    private function logWrongPasswordNotice(Entity\StandardIdentity $identity, $input)
267
    {
268 2
        $this->logger->notice('wrong password', [
269 2
            'input' => $input,
270
            'user' => [
271 2
                'account' => $identity->getAccountId(),
272 2
                'identity' => $identity->getId(),
273
            ],
274
        ]);
275 2
    }
276
277
278
    /**
279
     * @param string $message logged text
280
     */
281 4
    private function logExpectedBehaviour(Entity\Identity $identity, $message)
282
    {
283 4
        $this->logger->info($message, [
284
            'user' => [
285 4
                'account' => $identity->getAccountId(),
286 4
                'identity' => $identity->getId(),
287
            ],
288
        ]);
289 4
    }
290
291
292 3
    public function useNonceIdentity(Entity\NonceIdentity $identity, string $key): Entity\CookieIdentity
293
    {
294 3
        $this->checkIdentityExpireTime($identity, $this->assembleNonceLogDetails($identity));
295
296 2
        if ($identity->matchKey($key) === false) {
297 1
            $this->logger->notice('wrong key', $this->assembleNonceLogDetails($identity));
298 1
            throw new KeyMismatch;
299
        }
300
301 1
        $this->changeIdentityStatus($identity, Entity\Identity::STATUS_DISCARDED);
302 1
        $this->logExpectedBehaviour($identity, 'one-time identity used');
303
304 1
        return $this->createCookieIdentity($identity);
305
    }
306
307
308 3 View Code Duplication
    private function assembleNonceLogDetails(Entity\NonceIdentity $identity): array
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
309
    {
310
        return [
311
            'input' => [
312 3
                'identifier' => $identity->getIdentifier(),
313 3
                'key' => $identity->getKey(),
314
            ],
315
            'user' => [
316 3
                'account' => $identity->getAccountId(),
317 3
                'identity' => $identity->getId(),
318
            ],
319
        ];
320
    }
321
322
323
    public function markForUpdate(Entity\Identity $identity, array $payload, $tokenLifespan = self::DEFAULT_TOKEN_LIFESPAN)
324
    {
325
        $identity->generateToken();
326
        $identity->setTokenAction(Entity\Identity::ACTION_UPDATE);
327
        $identity->setTokenEndOfLife(time() + $tokenLifespan);
328
        $identity->setTokenPayload($payload);
329
330
        $this->repository->save($identity);
331
332
        $this->logger->info('request identity update', [
333
            'input' => [
334
                'id' => $identity->getId(),
335
            ],
336
        ]);
337
338
        return $identity->getToken();
339
    }
340
341
342
    public function updateStandardIdentity(Entity\StandardIdentity $identity)
343
    {
344
        $identity->clearToken();
345
        $this->repository->save($identity);
346
    }
347
}
348