Completed
Push — master ( 2eb8ac...4b6668 )
by Mārtiņš
02:21
created

Identification::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 12
ccs 6
cts 6
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 9
nc 1
nop 4
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\Mapper as Mapper;
10
use Palladium\Entity as Entity;
11
use Palladium\Exception\PasswordMismatch;
12
use Palladium\Exception\KeyMismatch;
13
use Palladium\Exception\CompromisedCookie;
14
use Palladium\Exception\IdentityExpired;
15
use Palladium\Contract\CanCreateMapper;
16
use Psr\Log\LoggerInterface;
17
18
class Identification
19
{
20
21
    const DEFAULT_COOKIE_LIFESPAN = 14400; // 4 hours
22
    const DEFAULT_HASH_COST = 12;
23
24
    private $mapperFactory;
25
    private $logger;
26
27
    private $cookieLifespan;
28
    private $hashCost;
29
30
    /**
31
     * @param CanCreateMapper $mapperFactory Factory for creating persistence layer structures
32
     * @param LoggerInterface $logger PSR-3 compatible logger
33
     * @param int $cookieLifespan Lifespan of the authentication cookie in seconds
34
     */
35 13
    public function __construct(
36
        CanCreateMapper $mapperFactory,
37
        LoggerInterface $logger,
38
        $cookieLifespan = self::DEFAULT_COOKIE_LIFESPAN,
39
        $hashCost = self::DEFAULT_HASH_COST
40
        )
41
    {
42 13
        $this->mapperFactory = $mapperFactory;
43 13
        $this->logger = $logger;
44 13
        $this->cookieLifespan = $cookieLifespan;
45 13
        $this->hashCost = $hashCost;
46 13
    }
47
48
49 2
    public function loginWithPassword(Entity\EmailIdentity $identity, string $password): Entity\CookieIdentity
50
    {
51 2
        if ($identity->matchPassword($password) === false) {
52 1
            $this->logWrongPasswordNotice($identity, [
53 1
                'email' => $identity->getEmailAddress(),
54 1
                'key' => md5($password),
55
            ]);
56
57 1
            throw new PasswordMismatch;
58
        }
59
60 1
        $this->updateEmailIdentityOnUse($identity);
61 1
        $cookie = $this->createCookieIdentity($identity);
62
63 1
        $this->logger->info('login successful', [
64
            'input' => [
65 1
                'email' => $identity->getEmailAddress(),
66
            ],
67
            'user' => [
68 1
                'account' => $identity->getAccountId(),
69 1
                'identity' => $identity->getId(),
70
            ],
71
        ]);
72
73 1
        return $cookie;
74
    }
75
76
77 1
    private function updateEmailIdentityOnUse(Entity\EmailIdentity $identity)
78
    {
79 1
        $mapper = $this->mapperFactory->create(Mapper\Identity::class);
80 1
        if ($identity->hasOldHash()) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
81
            // @TODO: choose a different mapper to save the hash
82
        }
83 1
        $identity->setLastUsed(time());
84
85 1
        $mapper->store($identity);
86 1
    }
87
88
89 2
    private function createCookieIdentity(Entity\Identity $identity): Entity\CookieIdentity
90
    {
91 2
        $cookie = new Entity\CookieIdentity;
92 2
        $mapper = $this->mapperFactory->create(Mapper\CookieIdentity::class);
93
94 2
        $cookie->setAccountId($identity->getAccountId());
95 2
        $cookie->generateNewSeries();
96
97 2
        $cookie->generateNewKey();
98 2
        $cookie->setStatus(Entity\Identity::STATUS_ACTIVE);
99 2
        $cookie->setExpiresOn(time() + $this->cookieLifespan);
100
101
102 2
        $parentId = $identity->getParentId();
103
104 2
        if (null === $parentId) {
105 2
            $parentId = $identity->getId();
106
        }
107
108 2
        $cookie->setParentId($parentId);
109
110 2
        $mapper->store($cookie);
111
112 2
        return $cookie;
113
    }
114
115
116
    /**
117
     * @param string @key
118
     *
119
     * @throws \Palladium\Exception\CompromisedCookie if key does not match
120
     * @throws \Palladium\Exception\IdentityExpired if cookie is too old
121
     */
122 3
    public function loginWithCookie(Entity\CookieIdentity $identity, $key): Entity\CookieIdentity
123
    {
124 3
        $this->checkIdentityExpireTime($identity, $this->assembleCookieLogDetails($identity));
125 2
        $this->checkCookieKey($identity, $key);
126
127 1
        $identity->generateNewKey();
128 1
        $identity->setLastUsed(time());
129 1
        $identity->setExpiresOn(time() + $this->cookieLifespan);
130
131 1
        $mapper = $this->mapperFactory->create(Mapper\CookieIdentity::class);
132 1
        $mapper->store($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 2
            $this->logger->info('identity expired', $details);
157
158 2
            $this->changeIdentityStatus($identity, Entity\Identity::STATUS_EXPIRED);
159
160 2
            throw new IdentityExpired;
161
        }
162 5
    }
163
164
165 5
    private function changeIdentityStatus(Entity\Identity $identity, int $status)
166
    {
167 5
        $identity->setStatus($status);
168 5
        $mapper = $this->mapperFactory->create(Mapper\Identity::class);
169 5
        $mapper->store($identity);
170 5
    }
171
172
173
    /**
174
     * Verify that the cookie based identity matches the key and,
175
     * if verification is failed, disable this given identity
176
     *
177
     * @param string $key
178
     * @throws \Palladium\Exception\CompromisedCookie if key does not match
179
     */
180 3
    private function checkCookieKey(Entity\CookieIdentity $identity, $key)
181
    {
182 3
        if ($identity->matchKey($key) === true) {
183 2
            return;
184
        }
185
186 1
        $this->changeIdentityStatus($identity, Entity\Identity::STATUS_BLOCKED);
187 1
        $this->logger->warning('compromised cookie', $this->assembleCookieLogDetails($identity));
188
189 1
        throw new CompromisedCookie;
190
    }
191
192
193 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...
194
    {
195
        return [
196
            'input' => [
197 4
                'account' => $identity->getAccountId(),
198 4
                'series' => $identity->getSeries(),
199 4
                'key' => md5($identity->getKey()),
200
            ],
201
            'user' => [
202 4
                'account' => $identity->getAccountId(),
203 4
                'identity' => $identity->getId(),
204
            ],
205
        ];
206
    }
207
208
209 1
    public function discardIdentityCollection(Entity\IdentityCollection $list)
210
    {
211 1
        foreach ($list as $identity) {
212 1
            $identity->setStatus(Entity\Identity::STATUS_DISCARDED);
213
        }
214
215 1
        $mapper = $this->mapperFactory->create(Mapper\IdentityCollection::class);
216 1
        $mapper->store($list);
217 1
    }
218
219
220 1
    public function blockIdentity(Entity\Identity $identity)
221
    {
222 1
        $identity->setStatus(Entity\Identity::STATUS_BLOCKED);
223
224 1
        $mapper = $this->mapperFactory->create(Mapper\Identity::class);
225 1
        $mapper->store($identity);
226 1
    }
227
228
229
    /**
230
     * @codeCoverageIgnore
231
     */
232
    public function deleteIdentity(Entity\Identity $identity)
233
    {
234
        $mapper = $this->mapperFactory->create(Mapper\Identity::class);
235
        $mapper->remove($identity);
236
    }
237
238
239
    /**
240
     * @param string $oldPassword
241
     * @param string $newPassword
242
     */
243 2
    public function changePassword(Entity\EmailIdentity $identity, $oldPassword, $newPassword)
244
    {
245 2
        $mapper = $this->mapperFactory->create(Mapper\EmailIdentity::class);
246
247 2
        if ($identity->matchPassword($oldPassword) === false) {
248 1
            $this->logWrongPasswordNotice($identity, [
249 1
                'account' => $identity->getAccountId(),
250 1
                'old-key' => md5($oldPassword),
251 1
                'new-key' => md5($newPassword),
252
            ]);
253
254 1
            throw new PasswordMismatch;
255
        }
256
257 1
        $identity->setPassword($newPassword);
258 1
        $mapper->store($identity);
259
260 1
        $this->logExpectedBehaviour($identity, 'password changed');
261 1
    }
262
263
264
    /**
265
     * @param array $input
266
     */
267 2
    private function logWrongPasswordNotice(Entity\EmailIdentity $identity, $input)
268
    {
269 2
        $this->logger->notice('wrong password', [
270 2
            'input' => $input,
271
            'user' => [
272 2
                'account' => $identity->getAccountId(),
273 2
                'identity' => $identity->getId(),
274
            ],
275
        ]);
276 2
    }
277
278
279
    /**
280
     * @param string $message logged text
281
     */
282 4
    private function logExpectedBehaviour(Entity\Identity $identity, $message)
283
    {
284 4
        $this->logger->info($message, [
285
            'user' => [
286 4
                'account' => $identity->getAccountId(),
287 4
                'identity' => $identity->getId(),
288
            ],
289
        ]);
290 4
    }
291
292
293 3
    public function useNonceIdentity(Entity\NonceIdentity $identity, string $key): Entity\CookieIdentity
294
    {
295 3
        $this->checkIdentityExpireTime($identity, $this->assembleNonceLogDetails($identity));
296
297 2
        if ($identity->matchKey($key) === false) {
298 1
            $this->logger->notice('wrong key', $this->assembleNonceLogDetails($identity));
299 1
            throw new KeyMismatch;
300
        }
301
302 1
        $this->changeIdentityStatus($identity, Entity\Identity::STATUS_DISCARDED);
303 1
        $this->logExpectedBehaviour($identity, 'one-time identity used');
304
305 1
        return $this->createCookieIdentity($identity);
306
    }
307
308
309 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...
310
    {
311
        return [
312
            'input' => [
313 3
                'identifier' => $identity->getIdentifier(),
314 3
                'key' => md5($identity->getKey()),
315
            ],
316
            'user' => [
317 3
                'account' => $identity->getAccountId(),
318 3
                'identity' => $identity->getId(),
319
            ],
320
        ];
321
    }
322
323
}
324