Completed
Branch master (3ebf23)
by Mārtiņš
03:14 queued 13s
created

Identification::blockIdentity()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 7
c 1
b 0
f 0
ccs 5
cts 5
cp 1
rs 9.4285
cc 1
eloc 4
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\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
23
    private $mapperFactory;
24
    private $logger;
25
26
    private $cookieLifespan;
27
28
    /**
29
     * @param CanCreateMapper $mapperFactory Factory for creating persistence layer structures
30
     * @param LoggerInterface $logger PSR-3 compatible logger
31
     * @param int $cookieLifespan Lifespan of the authentication cookie in seconds
32
     */
33 13
    public function __construct(CanCreateMapper $mapperFactory, LoggerInterface $logger, $cookieLifespan = self::DEFAULT_COOKIE_LIFESPAN)
34
    {
35 13
        $this->mapperFactory = $mapperFactory;
36 13
        $this->logger = $logger;
37 13
        $this->cookieLifespan = $cookieLifespan;
38 13
    }
39
40
41 2
    public function loginWithPassword(Entity\EmailIdentity $identity, string $password): Entity\CookieIdentity
42
    {
43 2
        if ($identity->matchPassword($password) === false) {
44 1
            $this->logWrongPasswordNotice($identity, [
45 1
                'email' => $identity->getEmailAddress(),
46 1
                'key' => md5($password),
47
            ]);
48
49 1
            throw new PasswordMismatch;
50
        }
51
52 1
        $this->registerUsageOfIdentity($identity);
53 1
        $cookie = $this->createCookieIdentity($identity);
54
55 1
        $this->logger->info('login successful', [
56
            'input' => [
57 1
                'email' => $identity->getEmailAddress(),
58
            ],
59
            'user' => [
60 1
                'account' => $identity->getAccountId(),
61 1
                'identity' => $identity->getId(),
62
            ],
63
        ]);
64
65 1
        return $cookie;
66
    }
67
68
69 1
    private function registerUsageOfIdentity(Entity\Identity $identity)
70
    {
71 1
        $identity->setLastUsed(time());
72
73 1
        $mapper = $this->mapperFactory->create(Mapper\Identity::class);
74 1
        $mapper->store($identity);
75 1
    }
76
77
78 2
    private function createCookieIdentity(Entity\Identity $identity): Entity\CookieIdentity
79
    {
80 2
        $cookie = new Entity\CookieIdentity;
81 2
        $mapper = $this->mapperFactory->create(Mapper\CookieIdentity::class);
82
83 2
        $cookie->setAccountId($identity->getAccountId());
84 2
        $cookie->generateNewSeries();
85
86 2
        $cookie->generateNewKey();
87 2
        $cookie->setStatus(Entity\Identity::STATUS_ACTIVE);
88 2
        $cookie->setExpiresOn(time() + $this->cookieLifespan);
89
90
91 2
        $parentId = $identity->getParentId();
92
93 2
        if (null === $parentId) {
94 2
            $parentId = $identity->getId();
95
        }
96
97 2
        $cookie->setParentId($parentId);
98
99 2
        $mapper->store($cookie);
100
101 2
        return $cookie;
102
    }
103
104
105
    /**
106
     * @param string @key
107
     *
108
     * @throws \Palladium\Exception\CompromisedCookie if key does not match
109
     * @throws \Palladium\Exception\IdentityExpired if cookie is too old
110
     */
111 3
    public function loginWithCookie(Entity\CookieIdentity $identity, $key): Entity\CookieIdentity
112
    {
113 3
        $this->checkIdentityExpireTime($identity, $this->assembleCookieLogDetails($identity));
114 2
        $this->checkCookieKey($identity, $key);
115
116 1
        $identity->generateNewKey();
117 1
        $identity->setLastUsed(time());
118 1
        $identity->setExpiresOn(time() + $this->cookieLifespan);
119
120 1
        $mapper = $this->mapperFactory->create(Mapper\CookieIdentity::class);
121 1
        $mapper->store($identity);
122
123 1
        $this->logExpectedBehaviour($identity, 'cookie updated');
124
125 1
        return $identity;
126
    }
127
128
129
    /**
130
     * @param string $key
131
     */
132 1
    public function logout(Entity\CookieIdentity $identity, $key)
133
    {
134 1
        $this->checkIdentityExpireTime($identity, $this->assembleCookieLogDetails($identity));
135 1
        $this->checkCookieKey($identity, $key);
136
137 1
        $this->changeIdentityStatus($identity, Entity\Identity::STATUS_DISCARDED);
138 1
        $this->logExpectedBehaviour($identity, 'logout successful');
139 1
    }
140
141
142 7
    private function checkIdentityExpireTime(Entity\Identity $identity, $details)
143
    {
144 7
        if ($identity->getExpiresOn() < time()) {
145 2
            $this->logger->info('identity expired', $details);
146
147 2
            $this->changeIdentityStatus($identity, Entity\Identity::STATUS_EXPIRED);
148
149 2
            throw new IdentityExpired;
150
        }
151 5
    }
152
153
154 5
    private function changeIdentityStatus(Entity\Identity $identity, int $status)
155
    {
156 5
        $identity->setStatus($status);
157 5
        $mapper = $this->mapperFactory->create(Mapper\Identity::class);
158 5
        $mapper->store($identity);
159 5
    }
160
161
162
    /**
163
     * Verify that the cookie based identity matches the key and,
164
     * if verification is failed, disable this given identity
165
     *
166
     * @param string $key
167
     * @throws \Palladium\Exception\CompromisedCookie if key does not match
168
     */
169 3
    private function checkCookieKey(Entity\CookieIdentity $identity, $key)
170
    {
171 3
        if ($identity->matchKey($key) === true) {
172 2
            return;
173
        }
174
175 1
        $this->changeIdentityStatus($identity, Entity\Identity::STATUS_BLOCKED);
176 1
        $this->logger->warning('compromised cookie', $this->assembleCookieLogDetails($identity));
177
178 1
        throw new CompromisedCookie;
179
    }
180
181
182 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...
183
    {
184
        return [
185
            'input' => [
186 4
                'account' => $identity->getAccountId(),
187 4
                'series' => $identity->getSeries(),
188 4
                'key' => md5($identity->getKey()),
189
            ],
190
            'user' => [
191 4
                'account' => $identity->getAccountId(),
192 4
                'identity' => $identity->getId(),
193
            ],
194
        ];
195
    }
196
197
198 1
    public function discardIdentityCollection(Entity\IdentityCollection $list)
199
    {
200 1
        foreach ($list as $identity) {
201 1
            $identity->setStatus(Entity\Identity::STATUS_DISCARDED);
202
        }
203
204 1
        $mapper = $this->mapperFactory->create(Mapper\IdentityCollection::class);
205 1
        $mapper->store($list);
206 1
    }
207
208
209 1
    public function blockIdentity(Entity\Identity $identity)
210
    {
211 1
        $identity->setStatus(Entity\Identity::STATUS_BLOCKED);
212
213 1
        $mapper = $this->mapperFactory->create(Mapper\Identity::class);
214 1
        $mapper->store($identity);
215 1
    }
216
217
218
    /**
219
     * @codeCoverageIgnore
220
     */
221
    public function deleteIdentity(Entity\Identity $identity)
222
    {
223
        $mapper = $this->mapperFactory->create(Mapper\Identity::class);
224
        $mapper->remove($identity);
225
    }
226
227
228
    /**
229
     * @param string $oldPassword
230
     * @param string $newPassword
231
     */
232 2
    public function changePassword(Entity\EmailIdentity $identity, $oldPassword, $newPassword)
233
    {
234 2
        $mapper = $this->mapperFactory->create(Mapper\EmailIdentity::class);
235
236 2
        if ($identity->matchPassword($oldPassword) === false) {
237 1
            $this->logWrongPasswordNotice($identity, [
238 1
                'account' => $identity->getAccountId(),
239 1
                'old-key' => md5($oldPassword),
240 1
                'new-key' => md5($newPassword),
241
            ]);
242
243 1
            throw new PasswordMismatch;
244
        }
245
246 1
        $identity->setPassword($newPassword);
247 1
        $mapper->store($identity);
248
249 1
        $this->logExpectedBehaviour($identity, 'password changed');
250 1
    }
251
252
253
    /**
254
     * @param array $input
255
     */
256 2
    private function logWrongPasswordNotice(Entity\EmailIdentity $identity, $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
    /**
269
     * @param string $message logged text
270
     */
271 4
    private function logExpectedBehaviour(Entity\Identity $identity, $message)
272
    {
273 4
        $this->logger->info($message, [
274
            'user' => [
275 4
                'account' => $identity->getAccountId(),
276 4
                'identity' => $identity->getId(),
277
            ],
278
        ]);
279 4
    }
280
281
282 3
    public function useNonceIdentity(Entity\NonceIdentity $identity, string $key): Entity\CookieIdentity
283
    {
284 3
        $this->checkIdentityExpireTime($identity, $this->assembleNonceLogDetails($identity));
285
286 2
        if ($identity->matchKey($key) === false) {
287 1
            $this->logger->notice('wrong key', $this->assembleNonceLogDetails($identity));
288 1
            throw new KeyMismatch;
289
        }
290
291 1
        $this->changeIdentityStatus($identity, Entity\Identity::STATUS_DISCARDED);
292 1
        $this->logExpectedBehaviour($identity, 'one-time identity used');
293
294 1
        return $this->createCookieIdentity($identity);
295
    }
296
297
298 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...
299
    {
300
        return [
301
            'input' => [
302 3
                'identifier' => $identity->getIdentifier(),
303 3
                'key' => md5($identity->getKey()),
304
            ],
305
            'user' => [
306 3
                'account' => $identity->getAccountId(),
307 3
                'identity' => $identity->getId(),
308
            ],
309
        ];
310
    }
311
312
}
313