Passed
Push — master ( 6620a6...508b23 )
by Mārtiņš
02:30
created

Identification::discardTokenPayload()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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