Identification::logWrongPasswordNotice()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 5
c 1
b 0
f 0
dl 0
loc 7
ccs 5
cts 5
cp 1
rs 10
cc 1
nc 1
nop 2
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 39
    public function __construct(
38
        Repository $repository,
39
        LoggerInterface $logger,
40
        int $cookieLifespan = Identification::DEFAULT_COOKIE_LIFESPAN,
41
        int $hashCost = Identification::DEFAULT_HASH_COST
42
        )
43
    {
44 39
        $this->repository = $repository;
45 39
        $this->logger = $logger;
46 39
        $this->cookieLifespan = $cookieLifespan;
47 39
        $this->hashCost = $hashCost;
48 39
    }
49
50
51 12
    public function loginWithPassword(Entity\StandardIdentity $identity, string $password): Entity\CookieIdentity
52
    {
53 12
        if ($identity->matchPassword($password) === false) {
54 2
            $this->logWrongPasswordNotice($identity, [
55 2
                'identifier' => $identity->getIdentifier(),
56 2
                'key' => $password,
57
                // this is the wrong password, if you store it in plain-text
58
                // then it becomes your responsibility
59
            ]);
60
61 2
            throw new PasswordMismatch;
62
        }
63
64 10
        $identity->setPassword($password);
65 10
        $this->updateStandardIdentityOnUse($identity);
66 10
        $cookie = $this->createCookieIdentity($identity);
67
68 10
        $this->logger->info('login successful', [
69
            'input' => [
70 10
                'identifier' => $identity->getIdentifier(),
71
            ],
72
            'user' => [
73 10
                'account' => $identity->getAccountId(),
74 10
                'identity' => $identity->getId(),
75
            ],
76
        ]);
77
78 10
        return $cookie;
79
    }
80
81
82 10
    private function updateStandardIdentityOnUse(Entity\StandardIdentity $identity)
83
    {
84 10
        $type = Entity\Identity::class;
85
86 10
        if ($identity->hasOldHash($this->hashCost)) {
87 2
            $identity->rehashPassword($this->hashCost);
88 2
            $type = Entity\StandardIdentity::class;
89
        }
90
91 10
        $identity->setLastUsed(time());
92 10
        $this->repository->save($identity, $type);
93 10
    }
94
95
96 12
    private function createCookieIdentity(Entity\Identity $identity): Entity\CookieIdentity
97
    {
98 12
        $cookie = new Entity\CookieIdentity;
99
100 12
        $cookie->setAccountId($identity->getAccountId());
101 12
        $cookie->generateNewSeries();
102
103 12
        $cookie->generateNewKey();
104 12
        $cookie->setStatus(Entity\Identity::STATUS_ACTIVE);
105 12
        $cookie->setExpiresOn(time() + $this->cookieLifespan);
106
107
108 12
        $parentId = $identity->getParentId();
109
110 12
        if (null === $parentId) {
111 12
            $parentId = $identity->getId();
112
        }
113
114 12
        $cookie->setParentId($parentId);
115 12
        $this->repository->save($cookie);
116
117 12
        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 8
    public function loginWithCookie(Entity\CookieIdentity $identity, string $key): Entity\CookieIdentity
126
    {
127 8
        $this->checkIdentityExpireTime($identity, $this->assembleCookieLogDetails($identity));
128 6
        $this->checkCookieKey($identity, $key);
129
130 3
        $identity->generateNewKey();
131 3
        $identity->setLastUsed(time());
132 3
        $identity->setExpiresOn(time() + $this->cookieLifespan);
133
134 3
        $this->repository->save($identity);
135
136 3
        $this->logExpectedBehaviour($identity, 'cookie updated');
137
138 3
        return $identity;
139
    }
140
141
142
    /**
143
     * @param string $key
144
     */
145 2
    public function logout(Entity\CookieIdentity $identity, $key)
146
    {
147 2
        $this->checkIdentityExpireTime($identity, $this->assembleCookieLogDetails($identity));
148 2
        $this->checkCookieKey($identity, $key);
149
150 2
        $this->changeIdentityStatus($identity, Entity\Identity::STATUS_DISCARDED);
151 2
        $this->logExpectedBehaviour($identity, 'logout successful');
152 2
    }
153
154
155 15
    private function checkIdentityExpireTime(Entity\Identity $identity, $details)
156
    {
157 15
        if ($identity->getExpiresOn() > time()) {
158 12
            return;
159
        }
160
161 3
        $this->logger->info('identity expired', $details);
162 3
        $this->changeIdentityStatus($identity, Entity\Identity::STATUS_EXPIRED);
163
164 3
        throw new IdentityExpired;
165
    }
166
167
168 10
    private function changeIdentityStatus(Entity\Identity $identity, int $status)
169
    {
170 10
        $identity->setStatus($status);
171 10
        $identity->setLastUsed(time());
172 10
        $this->repository->save($identity, Entity\Identity::class);
173 10
    }
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 8
    private function checkCookieKey(Entity\CookieIdentity $identity, string $key)
183
    {
184 8
        if ($identity->matchKey($key) === true) {
185 5
            return;
186
        }
187
188 3
        $this->changeIdentityStatus($identity, Entity\Identity::STATUS_BLOCKED);
189 3
        $this->logger->warning('compromised cookie', $this->assembleCookieLogDetails($identity));
190
191 3
        throw new CompromisedCookie;
192
    }
193
194
195 10
    private function assembleCookieLogDetails(Entity\CookieIdentity $identity): array
196
    {
197
        return [
198
            'input' => [
199 10
                'account' => $identity->getAccountId(),
200 10
                'series' => $identity->getSeries(),
201 10
                'key' => $identity->getKey(),
202
            ],
203
            'user' => [
204 10
                'account' => $identity->getAccountId(),
205 10
                '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, $this->hashCost);
251 1
        $this->repository->save($identity);
252
253 1
        $this->logExpectedBehaviour($identity, 'password changed');
254 1
    }
255
256
257 3
    private function logWrongPasswordNotice(Entity\StandardIdentity $identity, array $input)
258
    {
259 3
        $this->logger->notice('wrong password', [
260 3
            'input' => $input,
261
            'user' => [
262 3
                'account' => $identity->getAccountId(),
263 3
                'identity' => $identity->getId(),
264
            ],
265
        ]);
266 3
    }
267
268
269 8
    private function logExpectedBehaviour(Entity\Identity $identity, string $message)
270
    {
271 8
        $this->logger->info($message, [
272
            'user' => [
273 8
                'account' => $identity->getAccountId(),
274 8
                'identity' => $identity->getId(),
275
            ],
276
        ]);
277 8
    }
278
279
280 5
    public function useNonceIdentity(Entity\NonceIdentity $identity, string $key): Entity\CookieIdentity
281
    {
282 5
        $this->checkIdentityExpireTime($identity, $this->assembleNonceLogDetails($identity));
283
284 4
        if ($identity->matchKey($key) === false) {
285 2
            $this->logger->notice('wrong key', $this->assembleNonceLogDetails($identity));
286 2
            throw new KeyMismatch;
287
        }
288
289 2
        $this->changeIdentityStatus($identity, Entity\Identity::STATUS_DISCARDED);
290 2
        $this->logExpectedBehaviour($identity, 'one-time identity used');
291
292 2
        return $this->createCookieIdentity($identity);
293
    }
294
295
296 5
    private function assembleNonceLogDetails(Entity\NonceIdentity $identity): array
297
    {
298
        return [
299
            'input' => [
300 5
                'identifier' => $identity->getIdentifier(),
301 5
                'key' => $identity->getKey(),
302
            ],
303
            'user' => [
304 5
                'account' => $identity->getAccountId(),
305 5
                'identity' => $identity->getId(),
306
            ],
307
        ];
308
    }
309
310
311 1
    public function markForUpdate(Entity\Identity $identity, array $payload, int $tokenLifespan = Identification::DEFAULT_TOKEN_LIFESPAN): string
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 3
    public function applyTokenPayload(Entity\Identity $identity)
331
    {
332 3
        $payload = $identity->getTokenPayload();
333
334 3
        if (null === $payload) {
335 1
            throw new PayloadNotFound;
336
        }
337
338 2
        foreach ($payload as $key => $value) {
339 1
            $method = 'set' . str_replace('_', '', $key);
340 1
            if (method_exists($identity, $method)) {
341 1
                $identity->{$method}($value);
342
            }
343
        }
344
345 2
        $this->discardTokenPayload($identity);
346 2
    }
347
348
349 2
    public function discardTokenPayload(Entity\Identity $identity)
350
    {
351 2
        $identity->clearToken();
352 2
        $this->repository->save($identity);
353 2
    }
354
}
355