Passed
Push — master ( e8409c...1501ad )
by Mārtiņš
01:51
created

Identification::clearIdentityToken()   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\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 16
    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 16
        $this->repository = $repository;
44 16
        $this->logger = $logger;
45 16
        $this->cookieLifespan = $cookieLifespan;
46 16
        $this->hashCost = $hashCost;
47 16
    }
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
     * @throws \Palladium\Exception\CompromisedCookie if key does not match
122
     * @throws \Palladium\Exception\IdentityExpired if cookie is too old
123
     */
124 3
    public function loginWithCookie(Entity\CookieIdentity $identity, string $key): Entity\CookieIdentity
125
    {
126 3
        $this->checkIdentityExpireTime($identity, $this->assembleCookieLogDetails($identity));
127 2
        $this->checkCookieKey($identity, $key);
128
129 1
        $identity->generateNewKey();
130 1
        $identity->setLastUsed(time());
131 1
        $identity->setExpiresOn(time() + $this->cookieLifespan);
132
133 1
        $this->repository->save($identity);
134
135 1
        $this->logExpectedBehaviour($identity, 'cookie updated');
136
137 1
        return $identity;
138
    }
139
140
141
    /**
142
     * @param string $key
143
     */
144 1
    public function logout(Entity\CookieIdentity $identity, $key)
145
    {
146 1
        $this->checkIdentityExpireTime($identity, $this->assembleCookieLogDetails($identity));
147 1
        $this->checkCookieKey($identity, $key);
148
149 1
        $this->changeIdentityStatus($identity, Entity\Identity::STATUS_DISCARDED);
150 1
        $this->logExpectedBehaviour($identity, 'logout successful');
151 1
    }
152
153
154 7
    private function checkIdentityExpireTime(Entity\Identity $identity, $details)
155
    {
156 7
        if ($identity->getExpiresOn() > time()) {
157 5
            return;
158
        }
159
160 2
        $this->logger->info('identity expired', $details);
161 2
        $this->changeIdentityStatus($identity, Entity\Identity::STATUS_EXPIRED);
162
163 2
        throw new IdentityExpired;
164
    }
165
166
167 5
    private function changeIdentityStatus(Entity\Identity $identity, int $status)
168
    {
169 5
        $identity->setStatus($status);
170 5
        $identity->setLastUsed(time());
171 5
        $this->repository->save($identity, Entity\Identity::class);
172 5
    }
173
174
175
    /**
176
     * Verify that the cookie based identity matches the key and,
177
     * if verification is failed, disable this given identity
178
     *
179
     * @throws \Palladium\Exception\CompromisedCookie if key does not match
180
     */
181 3
    private function checkCookieKey(Entity\CookieIdentity $identity, string $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
    private function assembleCookieLogDetails(Entity\CookieIdentity $identity): array
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 2
    public function changePassword(Entity\StandardIdentity $identity, string $oldPassword, string $newPassword)
237
    {
238
239 2
        if ($identity->matchPassword($oldPassword) === false) {
240 1
            $this->logWrongPasswordNotice($identity, [
241 1
                'account' => $identity->getAccountId(),
242 1
                'old-key' => $oldPassword, // the wrong password
243 1
                'new-key' => $newPassword,
244
            ]);
245
246 1
            throw new PasswordMismatch;
247
        }
248
249 1
        $identity->setPassword($newPassword);
250 1
        $this->repository->save($identity);
251
252 1
        $this->logExpectedBehaviour($identity, 'password changed');
253 1
    }
254
255
256 2
    private function logWrongPasswordNotice(Entity\StandardIdentity $identity, array $input)
257
    {
258 2
        $this->logger->notice('wrong password', [
259 2
            'input' => $input,
260
            'user' => [
261 2
                'account' => $identity->getAccountId(),
262 2
                'identity' => $identity->getId(),
263
            ],
264
        ]);
265 2
    }
266
267
268 4
    private function logExpectedBehaviour(Entity\Identity $identity, string $message)
269
    {
270 4
        $this->logger->info($message, [
271
            'user' => [
272 4
                'account' => $identity->getAccountId(),
273 4
                'identity' => $identity->getId(),
274
            ],
275
        ]);
276 4
    }
277
278
279 3
    public function useNonceIdentity(Entity\NonceIdentity $identity, string $key): Entity\CookieIdentity
280
    {
281 3
        $this->checkIdentityExpireTime($identity, $this->assembleNonceLogDetails($identity));
282
283 2
        if ($identity->matchKey($key) === false) {
284 1
            $this->logger->notice('wrong key', $this->assembleNonceLogDetails($identity));
285 1
            throw new KeyMismatch;
286
        }
287
288 1
        $this->changeIdentityStatus($identity, Entity\Identity::STATUS_DISCARDED);
289 1
        $this->logExpectedBehaviour($identity, 'one-time identity used');
290
291 1
        return $this->createCookieIdentity($identity);
292
    }
293
294
295 3
    private function assembleNonceLogDetails(Entity\NonceIdentity $identity): array
296
    {
297
        return [
298
            'input' => [
299 3
                'identifier' => $identity->getIdentifier(),
300 3
                'key' => $identity->getKey(),
301
            ],
302
            'user' => [
303 3
                'account' => $identity->getAccountId(),
304 3
                'identity' => $identity->getId(),
305
            ],
306
        ];
307
    }
308
309
310 1
    public function markForUpdate(Entity\Identity $identity, array $payload, int $tokenLifespan = self::DEFAULT_TOKEN_LIFESPAN)
311
    {
312 1
        $identity->generateToken();
313 1
        $identity->setTokenAction(Entity\Identity::ACTION_UPDATE);
314 1
        $identity->setTokenEndOfLife(time() + $tokenLifespan);
315 1
        $identity->setTokenPayload($payload);
316
317 1
        $this->repository->save($identity);
318
319 1
        $this->logger->info('request identity update', [
320
            'input' => [
321 1
                'id' => $identity->getId(),
322
            ],
323
        ]);
324
325 1
        return $identity->getToken();
326
    }
327
328
329 1
    public function clearIdentityToken(Entity\Identity $identity)
330
    {
331 1
        $identity->clearToken();
332 1
        $this->repository->save($identity);
333 1
    }
334
}
335