Completed
Push — master ( 3e95af...2fb675 )
by Mārtiņš
02:00
created

Identification::changePassword()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 24
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 24
ccs 0
cts 14
cp 0
rs 8.9713
cc 2
eloc 14
nc 2
nop 3
crap 6
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\PasswordNotMatch;
12
use Palladium\Exception\CompromisedCookie;
13
use Palladium\Exception\DenialOfServiceAttempt;
14
use Palladium\Exception\IdentityExpired;
15
use Palladium\Contract\CanCreateMapper;
16
use Psr\Log\LoggerInterface;
17
18
class Identification
19
{
20
21
    private $mapperFactory;
22
    private $logger;
23
24
25 4
    public function __construct(CanCreateMapper $mapperFactory, LoggerInterface $logger)
26
    {
27 4
        $this->mapperFactory = $mapperFactory;
28 4
        $this->logger = $logger;
29 4
    }
30
31 1
    public function loginWithPassword(Entity\EmailIdentity $identity, $password)
32
    {
33 1
        if ($identity->matchPassword($password) === false) {
34
            $this->logWrongPasswordWarning($identity, [
35
                'identifier' => $identity->getIdentifier(),
36
                'key' => md5($password),
37
            ]);
38
39
            throw new PasswordNotMatch;
40
        }
41
42 1
        $this->registerUsageOfIdentity($identity);
43 1
        $cookie = $this->createCookieIdentity($identity);
44
45 1
        $this->logger->info('login successful', [
46
            'input' => [
47 1
                'identifier' => $identity->getIdentifier(),
48
            ],
49
            'account' => [
50 1
                'user' => $identity->getUserId(),
51 1
                'identity' => $identity->getId(),
52
            ],
53
        ]);
54
55 1
        return $cookie;
56
    }
57
58
59 1
    private function registerUsageOfIdentity(Entity\Identity $identity)
60
    {
61 1
        $identity->setLastUsed(time());
62
63 1
        $mapper = $this->mapperFactory->create(Mapper\Identity::class);
64 1
        $mapper->store($identity);
65 1
    }
66
67
68 1
    private function createCookieIdentity(Entity\EmailIdentity $identity)
69
    {
70 1
        $cookie = new Entity\CookieIdentity;
71 1
        $mapper = $this->mapperFactory->create(Mapper\CookieIdentity::class);
72
73 1
        $cookie->setUserId($identity->getUserId());
74 1
        $cookie->generateNewSeries();
75
76 1
        while ($mapper->exists($cookie)) {
77
            // just a failsafe, to prevent violation of constraint
78
            $cookie->generateNewSeries();
79
        }
80
81 1
        $cookie->generateNewKey();
82 1
        $cookie->setStatus(Entity\Identity::STATUS_ACTIVE);
83 1
        $cookie->setExpiresOn(time() + Entity\Identity::COOKIE_LIFESPAN);
84
85 1
        $mapper->store($cookie);
86
87 1
        return $cookie;
88
    }
89
90
91 3
    public function loginWithCookie(Entity\CookieIdentity $identity, $key)
92
    {
93 3
        if ($identity->getId() === null) {
94 1
            $this->logCookieError($identity, 'denial of service');
95 1
            throw new DenialOfServiceAttempt;
96
        }
97
98 2
        $mapper = $this->mapperFactory->create(Mapper\CookieIdentity::class);
99
100 2
        if ($identity->getExpiresOn() < time()) {
101 1
            $identity->setStatus(Entity\Identity::STATUS_EXPIRED);
102 1
            $mapper->store($identity);
103 1
            $this->logger->info('cookie expired', [
104
                'input' => [
105 1
                    'user' => $identity->getUserId(),
106 1
                    'series' => $identity->getSeries(),
107 1
                    'key' => $identity->getKey(),
108
                ],
109
                'account' => [
110 1
                    'user' => $identity->getUserId(),
111 1
                    'identity' => $identity->getId(),
112
                ],
113
            ]);
114
115 1
            throw new IdentityExpired;
116
        }
117
118 1
        $this->checkCookieKey($identity, $key);
119
120 1
        $identity->generateNewKey();
121 1
        $identity->setLastUsed(time());
122 1
        $identity->setExpiresOn(time() + Entity\Identity::COOKIE_LIFESPAN);
123
124 1
        $mapper->store($identity);
125
126 1
        $this->logger->info('cookie updated', [
127
            'account' => [
128 1
                'user' => $identity->getUserId(),
129 1
                'identity' => $identity->getId(),
130
            ],
131
        ]);
132
133 1
        return $identity;
134
    }
135
136
137
    public function logout(Entity\CookieIdentity $identity, $key)
138
    {
139
        if ($identity->getId() === null) {
140
            $this->logCookieError($identity, 'denial of service');
141
            throw new DenialOfServiceAttempt;
142
        }
143
144
        $this->checkCookieKey($identity, $key);
145
        $identity->setStatus(Entity\Identity::STATUS_DISCARDED);
146
147
        $mapper = $this->mapperFactory->create(Mapper\CookieIdentity::class);
148
        $mapper->store($identity);
149
150
        $this->logger->info('logout successful', [
151
            'account' => [
152
                'user' => $identity->getUserId(),
153
                'identity' => $identity->getId(),
154
            ],
155
        ]);
156
157
    }
158
159
160
    /**
161
     * Verify that the cookie based identity matches the key and,
162
     * if verification is failed, disable this given identity
163
     *
164
     * @param string $key
165
     * @throws \Palladium\Exception\CompromisedCookie if key does not match
166
     */
167 1
    private function checkCookieKey(Entity\CookieIdentity $identity, $key)
168
    {
169 1
        if ($identity->matchKey($key) === true) {
170 1
            return;
171
        }
172
173
        $identity->setStatus(Entity\Identity::STATUS_BLOCKED);
174
175
        $mapper = $this->mapperFactory->create(Mapper\CookieIdentity::class);
176
        $mapper->store($identity);
177
178
        $this->logCookieError($identity, 'compromised cookie');
179
180
        throw new CompromisedCookie;
181
    }
182
183
184
    public function discardRelatedCookies(Entity\Identity $identity)
185
    {
186
        /**
187
         * @NOTE: this operation might require transaction
188
         * or a change in how store() is implemnted in IdentityCollection mapper
189
         */
190
        $list = $this->retrieveIdenitiesByUserId($identity->getUserId(), Entity\Identity::TYPE_COOKIE);
191
192
        foreach ($list as $identity) {
193
            $identity->setStatus(Entity\Identity::STATUS_DISCARDED);
194
        }
195
196
        $mapper = $this->mapperFactory->create(Mapper\IdentityCollection::class);
197
        $mapper->store($list);
198
    }
199
200
201
    private function retrieveIdenitiesByUserId($userId, $type = Entity\Identity::TYPE_ANY, $status = Entity\Identity::STATUS_ACTIVE)
202
    {
203
        $collection = new Entity\IdentityCollection;
204
        $collection->forUserId($userId);
205
        $collection->forType($type);
206
        $collection->forStatus($status);
207
208
        $mapper = $this->mapperFactory->create(Mapper\IdentityCollection::class);
209
        $mapper->fetch($collection);
210
211
        return $collection;
212
    }
213
214
215
    public function changePassword(Entity\EmailIdentity $identity, $oldPassword, $newPassword)
216
    {
217
        $mapper = $this->mapperFactory->create(Mapper\EmailIdentity::class);
218
219
        if ($identity->matchPassword($oldPassword) === false) {
220
            $this->logWrongPasswordWarning($identity, [
221
                'user' => $identity->getUserId(),
222
                'old-key' => md5($oldPassword),
223
                'new-key' => md5($newPassword),
224
            ]);
225
226
            throw new PasswordNotMatch;
227
        }
228
229
        $identity->setPassword($newPassword);
230
        $mapper->store($identity);
231
232
        $this->logger->info('password changed', [
233
            'account' => [
234
                'user' => $identity->getUserId(),
235
                'identity' => $identity->getId(),
236
            ],
237
        ]);
238
    }
239
240
241
    /**
242
     * @param string $message
243
     */
244 1
    private function logCookieError(Entity\CookieIdentity $identity, $message)
245
    {
246 1
        $this->logger->error($message, [
247
            'input' => [
248 1
                'user' => $identity->getUserId(),
249 1
                'series' => $identity->getSeries(),
250 1
                'key' => $identity->getKey(),
251
            ],
252
            'account' => [
253 1
                'user' => $identity->getUserId(),
254 1
                'identity' => $identity->getId(),
255
            ],
256
        ]);
257 1
    }
258
259
260
    /**
261
     * @param array $input
262
     */
263
    private function logWrongPasswordWarning(Entity\EmailIdentity $identity, $input)
264
    {
265
        $this->logger->warning('wrong password', [
266
            'input' => $input,
267
            'account' => [
268
                'user' => $identity->getUserId(),
269
                'identity' => $identity->getId(),
270
            ],
271
        ]);
272
    }
273
}
274