Passed
Push — master ( 53eeea...c4621c )
by Mārtiņš
02:52
created

Identification   A

Complexity

Total Complexity 28

Size/Duplication

Total Lines 318
Duplicated Lines 0 %

Test Coverage

Coverage 91.11%

Importance

Changes 0
Metric Value
wmc 28
dl 0
loc 318
c 0
b 0
f 0
ccs 123
cts 135
cp 0.9111
rs 10

20 Methods

Rating   Name   Duplication   Size   Complexity  
A assembleNonceLogDetails() 0 10 1
A checkCookieKey() 0 10 2
A logWrongPasswordNotice() 0 7 1
A blockIdentity() 0 4 1
A loginWithCookie() 0 14 1
A markForUpdate() 0 16 1
A createCookieIdentity() 0 22 2
A checkIdentityExpireTime() 0 10 2
A updateStandardIdentity() 0 4 1
B loginWithPassword() 0 28 2
A useNonceIdentity() 0 13 2
A updateStandardIdentityOnUse() 0 11 2
A assembleCookieLogDetails() 0 11 1
A logout() 0 7 1
A changePassword() 0 17 2
A __construct() 0 11 1
A deleteIdentity() 0 3 1
A logExpectedBehaviour() 0 6 1
A changeIdentityStatus() 0 5 1
A discardIdentityCollection() 0 7 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 Repository $repository Repository for abstracting persistence layer structures
32
     * @param 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
    public function __construct(
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
     */
0 ignored issues
show
Documentation Bug introduced by
The doc comment @key at position 0 could not be parsed: Unknown type name '@key' at position 0 in @key.
Loading history...
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
     * @throws \Palladium\Exception\CompromisedCookie if key does not match
182
     */
183 3
    private function checkCookieKey(Entity\CookieIdentity $identity, string $key)
184
    {
185 3
        if ($identity->matchKey($key) === true) {
186 2
            return;
187
        }
188
189 1
        $this->changeIdentityStatus($identity, Entity\Identity::STATUS_BLOCKED);
190 1
        $this->logger->warning('compromised cookie', $this->assembleCookieLogDetails($identity));
191
192 1
        throw new CompromisedCookie;
193
    }
194
195
196 4
    private function assembleCookieLogDetails(Entity\CookieIdentity $identity): array
197
    {
198
        return [
199
            'input' => [
200 4
                'account' => $identity->getAccountId(),
201 4
                'series' => $identity->getSeries(),
202 4
                'key' => $identity->getKey(),
203
            ],
204
            'user' => [
205 4
                'account' => $identity->getAccountId(),
206 4
                'identity' => $identity->getId(),
207
            ],
208
        ];
209
    }
210
211
212 1
    public function discardIdentityCollection(Entity\IdentityCollection $list)
213
    {
214 1
        foreach ($list as $identity) {
215 1
            $identity->setStatus(Entity\Identity::STATUS_DISCARDED);
216
        }
217
218 1
        $this->repository->save($list);
219 1
    }
220
221
222 1
    public function blockIdentity(Entity\Identity $identity)
223
    {
224 1
        $identity->setStatus(Entity\Identity::STATUS_BLOCKED);
225 1
        $this->repository->save($identity, Entity\Identity::class);
226 1
    }
227
228
229
    /**
230
     * @codeCoverageIgnore
231
     */
232
    public function deleteIdentity(Entity\Identity $identity)
233
    {
234
        $this->repository->delete($identity, Entity\Identity::class);
235
    }
236
237
238 2
    public function changePassword(Entity\StandardIdentity $identity, string $oldPassword, string $newPassword)
239
    {
240
241 2
        if ($identity->matchPassword($oldPassword) === false) {
242 1
            $this->logWrongPasswordNotice($identity, [
243 1
                'account' => $identity->getAccountId(),
244 1
                'old-key' => $oldPassword, // the wrong password
245 1
                'new-key' => $newPassword,
246
            ]);
247
248 1
            throw new PasswordMismatch;
249
        }
250
251 1
        $identity->setPassword($newPassword);
252 1
        $this->repository->save($identity);
253
254 1
        $this->logExpectedBehaviour($identity, 'password changed');
255 1
    }
256
257
258 2
    private function logWrongPasswordNotice(Entity\StandardIdentity $identity, array $input)
259
    {
260 2
        $this->logger->notice('wrong password', [
261 2
            'input' => $input,
262
            'user' => [
263 2
                'account' => $identity->getAccountId(),
264 2
                'identity' => $identity->getId(),
265
            ],
266
        ]);
267 2
    }
268
269
270 4
    private function logExpectedBehaviour(Entity\Identity $identity, string $message)
271
    {
272 4
        $this->logger->info($message, [
273
            'user' => [
274 4
                'account' => $identity->getAccountId(),
275 4
                'identity' => $identity->getId(),
276
            ],
277
        ]);
278 4
    }
279
280
281 3
    public function useNonceIdentity(Entity\NonceIdentity $identity, string $key): Entity\CookieIdentity
282
    {
283 3
        $this->checkIdentityExpireTime($identity, $this->assembleNonceLogDetails($identity));
284
285 2
        if ($identity->matchKey($key) === false) {
286 1
            $this->logger->notice('wrong key', $this->assembleNonceLogDetails($identity));
287 1
            throw new KeyMismatch;
288
        }
289
290 1
        $this->changeIdentityStatus($identity, Entity\Identity::STATUS_DISCARDED);
291 1
        $this->logExpectedBehaviour($identity, 'one-time identity used');
292
293 1
        return $this->createCookieIdentity($identity);
294
    }
295
296
297 3
    private function assembleNonceLogDetails(Entity\NonceIdentity $identity): array
298
    {
299
        return [
300
            'input' => [
301 3
                'identifier' => $identity->getIdentifier(),
302 3
                'key' => $identity->getKey(),
303
            ],
304
            'user' => [
305 3
                'account' => $identity->getAccountId(),
306 3
                'identity' => $identity->getId(),
307
            ],
308
        ];
309
    }
310
311
312
    public function markForUpdate(Entity\Identity $identity, array $payload, int $tokenLifespan = self::DEFAULT_TOKEN_LIFESPAN)
313
    {
314
        $identity->generateToken();
315
        $identity->setTokenAction(Entity\Identity::ACTION_UPDATE);
316
        $identity->setTokenEndOfLife(time() + $tokenLifespan);
317
        $identity->setTokenPayload($payload);
318
319
        $this->repository->save($identity);
320
321
        $this->logger->info('request identity update', [
322
            'input' => [
323
                'id' => $identity->getId(),
324
            ],
325
        ]);
326
327
        return $identity->getToken();
328
    }
329
330
331
    public function updateStandardIdentity(Entity\StandardIdentity $identity)
332
    {
333
        $identity->clearToken();
334
        $this->repository->save($identity);
335
    }
336
}
337