Completed
Push — master ( 734e62...750475 )
by Mārtiņš
03:48
created

Identification::loginWithPassword()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 27
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 27
ccs 14
cts 14
cp 1
rs 8.8571
cc 2
eloc 16
nc 2
nop 2
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_HASH_COST = 12;
22
23
    private $repository;
24
    private $logger;
25
26
    private $cookieLifespan;
27
    private $hashCost;
28
29
    /**
30
     * @param Palladium\Repository\Identity $repository Repository for abstracting persistence layer structures
31
     * @param Psr\Log\LoggerInterface $logger PSR-3 compatible logger
32
     * @param int $cookieLifespan Lifespan of the authentication cookie in seconds (default: 4 hours)
33
     * @param int $hashCost Cost of the bcrypt hashing function (default: 12)
34
     */
35 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...
36
        Repository $repository,
37
        LoggerInterface $logger,
38
        int $cookieLifespan = self::DEFAULT_COOKIE_LIFESPAN,
39
        int $hashCost = self::DEFAULT_HASH_COST
40
        )
41
    {
42 14
        $this->repository = $repository;
43 14
        $this->logger = $logger;
44 14
        $this->cookieLifespan = $cookieLifespan;
45 14
        $this->hashCost = $hashCost;
46 14
    }
47
48
49 3
    public function loginWithPassword(Entity\StandardIdentity $identity, string $password): Entity\CookieIdentity
50
    {
51 3
        if ($identity->matchPassword($password) === false) {
52 1
            $this->logWrongPasswordNotice($identity, [
53 1
                'identifier' => $identity->getIdentifier(),
54 1
                'key' => $password, // this is the wrong password
55
            ]);
56
57 1
            throw new PasswordMismatch;
58
        }
59
60 2
        $identity->setPassword($password);
61 2
        $this->updateStandardIdentityOnUse($identity);
62 2
        $cookie = $this->createCookieIdentity($identity);
63
64 2
        $this->logger->info('login successful', [
65
            'input' => [
66 2
                'identifier' => $identity->getIdentifier(),
67
            ],
68
            'user' => [
69 2
                'account' => $identity->getAccountId(),
70 2
                'identity' => $identity->getId(),
71
            ],
72
        ]);
73
74 2
        return $cookie;
75
    }
76
77
78 2
    private function updateStandardIdentityOnUse(Entity\StandardIdentity $identity)
79
    {
80 2
        $type = Entity\Identity::class;
81
82 2
        if ($identity->hasOldHash($this->hashCost)) {
83 1
            $identity->rehashPassword($this->hashCost);
84 1
            $type = Entity\StandardIdentity::class;
85
        }
86
87 2
        $identity->setLastUsed(time());
88 2
        $this->repository->save($identity, $type);
89 2
    }
90
91
92 3
    private function createCookieIdentity(Entity\Identity $identity): Entity\CookieIdentity
93
    {
94 3
        $cookie = new Entity\CookieIdentity;
95
96 3
        $cookie->setAccountId($identity->getAccountId());
97 3
        $cookie->generateNewSeries();
98
99 3
        $cookie->generateNewKey();
100 3
        $cookie->setStatus(Entity\Identity::STATUS_ACTIVE);
101 3
        $cookie->setExpiresOn(time() + $this->cookieLifespan);
102
103
104 3
        $parentId = $identity->getParentId();
105
106 3
        if (null === $parentId) {
107 3
            $parentId = $identity->getId();
108
        }
109
110 3
        $cookie->setParentId($parentId);
111 3
        $this->repository->save($cookie);
112
113 3
        return $cookie;
114
    }
115
116
117
    /**
118
     * @param string @key
119
     *
120
     * @throws \Palladium\Exception\CompromisedCookie if key does not match
121
     * @throws \Palladium\Exception\IdentityExpired if cookie is too old
122
     */
123 3
    public function loginWithCookie(Entity\CookieIdentity $identity, $key): Entity\CookieIdentity
124
    {
125 3
        $this->checkIdentityExpireTime($identity, $this->assembleCookieLogDetails($identity));
126 2
        $this->checkCookieKey($identity, $key);
127
128 1
        $identity->generateNewKey();
129 1
        $identity->setLastUsed(time());
130 1
        $identity->setExpiresOn(time() + $this->cookieLifespan);
131
132 1
        $this->repository->save($identity);
133
134 1
        $this->logExpectedBehaviour($identity, 'cookie updated');
135
136 1
        return $identity;
137
    }
138
139
140
    /**
141
     * @param string $key
142
     */
143 1
    public function logout(Entity\CookieIdentity $identity, $key)
144
    {
145 1
        $this->checkIdentityExpireTime($identity, $this->assembleCookieLogDetails($identity));
146 1
        $this->checkCookieKey($identity, $key);
147
148 1
        $this->changeIdentityStatus($identity, Entity\Identity::STATUS_DISCARDED);
149 1
        $this->logExpectedBehaviour($identity, 'logout successful');
150 1
    }
151
152
153 7
    private function checkIdentityExpireTime(Entity\Identity $identity, $details)
154
    {
155 7
        if ($identity->getExpiresOn() > time()) {
156 5
            return;
157
        }
158
159 2
        $this->logger->info('identity expired', $details);
160 2
        $this->changeIdentityStatus($identity, Entity\Identity::STATUS_EXPIRED);
161
162 2
        throw new IdentityExpired;
163
    }
164
165
166 5
    private function changeIdentityStatus(Entity\Identity $identity, int $status)
167
    {
168 5
        $identity->setStatus($status);
169 5
        $identity->setLastUsed(time());
170 5
        $this->repository->save($identity, Entity\Identity::class);
171 5
    }
172
173
174
    /**
175
     * Verify that the cookie based identity matches the key and,
176
     * if verification is failed, disable this given identity
177
     *
178
     * @param string $key
179
     * @throws \Palladium\Exception\CompromisedCookie if key does not match
180
     */
181 3
    private function checkCookieKey(Entity\CookieIdentity $identity, $key)
182
    {
183 3
        if ($identity->matchKey($key) === true) {
184 2
            return;
185
        }
186
187 1
        $this->changeIdentityStatus($identity, Entity\Identity::STATUS_BLOCKED);
188 1
        $this->logger->warning('compromised cookie', $this->assembleCookieLogDetails($identity));
189
190 1
        throw new CompromisedCookie;
191
    }
192
193
194 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...
195
    {
196
        return [
197
            'input' => [
198 4
                'account' => $identity->getAccountId(),
199 4
                'series' => $identity->getSeries(),
200 4
                'key' => $identity->getKey(),
201
            ],
202
            'user' => [
203 4
                'account' => $identity->getAccountId(),
204 4
                'identity' => $identity->getId(),
205
            ],
206
        ];
207
    }
208
209
210 1
    public function discardIdentityCollection(Entity\IdentityCollection $list)
211
    {
212 1
        foreach ($list as $identity) {
213 1
            $identity->setStatus(Entity\Identity::STATUS_DISCARDED);
214
        }
215
216 1
        $this->repository->save($list);
217 1
    }
218
219
220 1
    public function blockIdentity(Entity\Identity $identity)
221
    {
222 1
        $identity->setStatus(Entity\Identity::STATUS_BLOCKED);
223 1
        $this->repository->save($identity, Entity\Identity::class);
224 1
    }
225
226
227
    /**
228
     * @codeCoverageIgnore
229
     */
230
    public function deleteIdentity(Entity\Identity $identity)
231
    {
232
        $this->repository->delete($identity, Entity\Identity::class);
233
    }
234
235
236
    /**
237
     * @param string $oldPassword
238
     * @param string $newPassword
239
     */
240 2
    public function changePassword(Entity\StandardIdentity $identity, $oldPassword, $newPassword)
241
    {
242
243 2
        if ($identity->matchPassword($oldPassword) === false) {
244 1
            $this->logWrongPasswordNotice($identity, [
245 1
                'account' => $identity->getAccountId(),
246 1
                'old-key' => $oldPassword, // the wrong password
247 1
                'new-key' => $newPassword,
248
            ]);
249
250 1
            throw new PasswordMismatch;
251
        }
252
253 1
        $identity->setPassword($newPassword);
254 1
        $this->repository->save($identity);
255
256 1
        $this->logExpectedBehaviour($identity, 'password changed');
257 1
    }
258
259
260
    /**
261
     * @param array $input
262
     */
263 2
    private function logWrongPasswordNotice(Entity\StandardIdentity $identity, $input)
264
    {
265 2
        $this->logger->notice('wrong password', [
266 2
            'input' => $input,
267
            'user' => [
268 2
                'account' => $identity->getAccountId(),
269 2
                'identity' => $identity->getId(),
270
            ],
271
        ]);
272 2
    }
273
274
275
    /**
276
     * @param string $message logged text
277
     */
278 4
    private function logExpectedBehaviour(Entity\Identity $identity, $message)
279
    {
280 4
        $this->logger->info($message, [
281
            'user' => [
282 4
                'account' => $identity->getAccountId(),
283 4
                'identity' => $identity->getId(),
284
            ],
285
        ]);
286 4
    }
287
288
289 3
    public function useNonceIdentity(Entity\NonceIdentity $identity, string $key): Entity\CookieIdentity
290
    {
291 3
        $this->checkIdentityExpireTime($identity, $this->assembleNonceLogDetails($identity));
292
293 2
        if ($identity->matchKey($key) === false) {
294 1
            $this->logger->notice('wrong key', $this->assembleNonceLogDetails($identity));
295 1
            throw new KeyMismatch;
296
        }
297
298 1
        $this->changeIdentityStatus($identity, Entity\Identity::STATUS_DISCARDED);
299 1
        $this->logExpectedBehaviour($identity, 'one-time identity used');
300
301 1
        return $this->createCookieIdentity($identity);
302
    }
303
304
305 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...
306
    {
307
        return [
308
            'input' => [
309 3
                'identifier' => $identity->getIdentifier(),
310 3
                'key' => $identity->getKey(),
311
            ],
312
            'user' => [
313 3
                'account' => $identity->getAccountId(),
314 3
                'identity' => $identity->getId(),
315
            ],
316
        ];
317
    }
318
319
}
320