|
1
|
|
|
<?php |
|
2
|
|
|
|
|
3
|
|
|
namespace Palladium\Service; |
|
4
|
|
|
|
|
5
|
|
|
/** |
|
6
|
|
|
* Retrieval and handling of identities for registered users |
|
7
|
|
|
*/ |
|
8
|
|
|
|
|
9
|
|
|
use RuntimeException; |
|
10
|
|
|
|
|
11
|
|
|
use Palladium\Component\MapperFactory; |
|
12
|
|
|
use Palladium\Mapper\Authentication as Mapper; |
|
13
|
|
|
use Palladium\Entity\Authentication as Entity; |
|
14
|
|
|
|
|
15
|
|
|
use Palladium\Exception\IdentityDuplicated; |
|
16
|
|
|
use Palladium\Exception\IdentityNotFound; |
|
17
|
|
|
use Palladium\Exception\EmailNotFound; |
|
18
|
|
|
use Palladium\Exception\PasswordNotMatch; |
|
19
|
|
|
use Palladium\Exception\CompromisedCookie; |
|
20
|
|
|
use Palladium\Exception\DenialOfServiceAttempt; |
|
21
|
|
|
use Palladium\Exception\IdentityExpired; |
|
22
|
|
|
use Palladium\Exception\Community\UserNotFound; |
|
23
|
|
|
|
|
24
|
|
|
|
|
25
|
|
|
class Identification extends Locator |
|
26
|
|
|
{ |
|
27
|
|
|
|
|
28
|
|
|
private $currentCookie; |
|
29
|
|
|
|
|
30
|
|
|
|
|
31
|
|
|
public function loginWithPassword($identifier, $key) |
|
32
|
|
|
{ |
|
33
|
|
|
$identity = $this->retrievePasswordIdenityByIdentifier($identifier); |
|
34
|
|
|
|
|
35
|
|
|
if ($identity->getId() === null) { |
|
36
|
|
|
// hardening against timeing based side-channel attacks |
|
37
|
|
|
$identity->setKey(''); |
|
38
|
|
|
|
|
39
|
|
|
$this->logger->warning('acount not found', [ |
|
40
|
|
|
'input' => [ |
|
41
|
|
|
'identifier' => $identifier, |
|
42
|
|
|
'key' => md5($key), |
|
43
|
|
|
], |
|
44
|
|
|
'account' => [ |
|
45
|
|
|
'user' => null, |
|
46
|
|
|
'identity' => null, |
|
47
|
|
|
], |
|
48
|
|
|
]); |
|
49
|
|
|
|
|
50
|
|
|
throw new EmailNotFound; |
|
51
|
|
|
} |
|
52
|
|
|
|
|
53
|
|
View Code Duplication |
if ($identity->matchKey($key) === false) { |
|
|
|
|
|
|
54
|
|
|
$this->logger->warning('wrong password', [ |
|
55
|
|
|
'input' => [ |
|
56
|
|
|
'identifier' => $identifier, |
|
57
|
|
|
'key' => md5($key), |
|
58
|
|
|
], |
|
59
|
|
|
'account' => [ |
|
60
|
|
|
'user' => $identity->getUserId(), |
|
61
|
|
|
'identity' => $identity->getId(), |
|
62
|
|
|
], |
|
63
|
|
|
]); |
|
64
|
|
|
|
|
65
|
|
|
throw new PasswordNotMatch; |
|
66
|
|
|
} |
|
67
|
|
|
|
|
68
|
|
|
$this->registerUsageOfIdentity($identity); |
|
69
|
|
|
$cookie = $this->createCookieIdentity($identity); |
|
70
|
|
|
|
|
71
|
|
|
$this->logger->info('login successful', [ |
|
72
|
|
|
'input' => [ |
|
73
|
|
|
'identifier' => $identifier, |
|
74
|
|
|
], |
|
75
|
|
|
'account' => [ |
|
76
|
|
|
'user' => $identity->getUserId(), |
|
77
|
|
|
'identity' => $identity->getId(), |
|
78
|
|
|
], |
|
79
|
|
|
]); |
|
80
|
|
|
|
|
81
|
|
|
$this->currentCookie = $cookie; |
|
82
|
|
|
} |
|
83
|
|
|
|
|
84
|
|
|
|
|
85
|
|
|
private function registerUsageOfIdentity(Entity\Identity $identity) |
|
86
|
|
|
{ |
|
87
|
|
|
$identity->setLastUsed(time()); |
|
88
|
|
|
|
|
89
|
|
|
$mapper = $this->mapperFactory->create(Mapper\Identity::class); |
|
90
|
|
|
$mapper->store($identity); |
|
91
|
|
|
} |
|
92
|
|
|
|
|
93
|
|
|
|
|
94
|
|
|
private function createCookieIdentity(Entity\PasswordIdentity $identity) |
|
95
|
|
|
{ |
|
96
|
|
|
$cookie = new Entity\CookieIdentity; |
|
97
|
|
|
$mapper = $this->mapperFactory->create(Mapper\CookieIdentity::class); |
|
98
|
|
|
|
|
99
|
|
|
$cookie->setUserId($identity->getUserId()); |
|
100
|
|
|
$cookie->generateNewSeries(); |
|
101
|
|
|
|
|
102
|
|
|
while ($mapper->exists($cookie)) { |
|
103
|
|
|
// just a failsafe, to prevent violation of constraint |
|
104
|
|
|
$cookie->generateNewSeries(); |
|
105
|
|
|
} |
|
106
|
|
|
|
|
107
|
|
|
$cookie->generateNewKey(); |
|
108
|
|
|
$cookie->setStatus(Entity\Identity::STATUS_ACTIVE); |
|
109
|
|
|
$cookie->setExpiresOn(time() + Entity\Identity::COOKIE_LIFESPAN); |
|
110
|
|
|
|
|
111
|
|
|
$mapper->store($cookie); |
|
112
|
|
|
|
|
113
|
|
|
return $cookie; |
|
114
|
|
|
} |
|
115
|
|
|
|
|
116
|
|
|
|
|
117
|
|
|
public function authenticateWithCookie($userId, $series, $key) |
|
118
|
|
|
{ |
|
119
|
|
|
$identity = $this->retrieveIdenityByCookie($userId, $series, Entity\Identity::STATUS_ACTIVE); |
|
120
|
|
|
|
|
121
|
|
View Code Duplication |
if ($identity->getId() === null) { |
|
|
|
|
|
|
122
|
|
|
$this->logger->error('denial of service', [ |
|
123
|
|
|
'input' => [ |
|
124
|
|
|
'user' => $userId, |
|
125
|
|
|
'series' => $series, |
|
126
|
|
|
'key' => $key, |
|
127
|
|
|
], |
|
128
|
|
|
'account' => [ |
|
129
|
|
|
'user' => $identity->getUserId(), |
|
130
|
|
|
'identity' => $identity->getId(), |
|
131
|
|
|
], |
|
132
|
|
|
]); |
|
133
|
|
|
|
|
134
|
|
|
throw new DenialOfServiceAttempt; |
|
135
|
|
|
} |
|
136
|
|
|
|
|
137
|
|
|
$mapper = $this->mapperFactory->create(Mapper\CookieIdentity::class); |
|
138
|
|
|
|
|
139
|
|
View Code Duplication |
if ($identity->getExpiresOn() < time()) { |
|
|
|
|
|
|
140
|
|
|
$identity->setStatus(Entity\Identity::STATUS_EXPIRED); |
|
141
|
|
|
$mapper->store($identity); |
|
142
|
|
|
$this->logger->info('cookie expired', [ |
|
143
|
|
|
'input' => [ |
|
144
|
|
|
'user' => $userId, |
|
145
|
|
|
'series' => $series, |
|
146
|
|
|
'key' => $key, |
|
147
|
|
|
], |
|
148
|
|
|
'account' => [ |
|
149
|
|
|
'user' => $identity->getUserId(), |
|
150
|
|
|
'identity' => $identity->getId(), |
|
151
|
|
|
], |
|
152
|
|
|
]); |
|
153
|
|
|
|
|
154
|
|
|
throw new IdentityExpired; |
|
155
|
|
|
} |
|
156
|
|
|
|
|
157
|
|
View Code Duplication |
if ($identity->matchKey($key) === false) { |
|
|
|
|
|
|
158
|
|
|
$identity->setStatus(Entity\Identity::STATUS_BLOCKED); |
|
159
|
|
|
$mapper->store($identity); |
|
160
|
|
|
|
|
161
|
|
|
$this->logger->error('compromised cookie', [ |
|
162
|
|
|
'input' => [ |
|
163
|
|
|
'user' => $userId, |
|
164
|
|
|
'series' => $series, |
|
165
|
|
|
'key' => $key, |
|
166
|
|
|
], |
|
167
|
|
|
'account' => [ |
|
168
|
|
|
'user' => $identity->getUserId(), |
|
169
|
|
|
'identity' => $identity->getId(), |
|
170
|
|
|
], |
|
171
|
|
|
]); |
|
172
|
|
|
|
|
173
|
|
|
throw new CompromisedCookie; |
|
174
|
|
|
} |
|
175
|
|
|
|
|
176
|
|
|
$identity->generateNewKey(); |
|
177
|
|
|
$identity->setLastUsed(time()); |
|
178
|
|
|
$identity->setExpiresOn(time() + Entity\Identity::COOKIE_LIFESPAN); |
|
179
|
|
|
|
|
180
|
|
|
$mapper->store($identity); |
|
181
|
|
|
|
|
182
|
|
|
$this->logger->info('cookie updated', [ |
|
183
|
|
|
'account' => [ |
|
184
|
|
|
'user' => $identity->getUserId(), |
|
185
|
|
|
'identity' => $identity->getId(), |
|
186
|
|
|
], |
|
187
|
|
|
]); |
|
188
|
|
|
|
|
189
|
|
|
$this->currentCookie = $identity; |
|
190
|
|
|
} |
|
191
|
|
|
|
|
192
|
|
|
|
|
193
|
|
|
private function retrieveIdenityByCookie($userId, $series, $status = Entity\Identity::STATUS_ANY) |
|
194
|
|
|
{ |
|
195
|
|
|
$cookie = new Entity\CookieIdentity; |
|
196
|
|
|
$mapper = $this->mapperFactory->create(Mapper\CookieIdentity::class); |
|
197
|
|
|
|
|
198
|
|
|
$cookie->setUserId($userId); |
|
199
|
|
|
$cookie->setSeries($series); |
|
200
|
|
|
$cookie->setStatus($status); |
|
201
|
|
|
|
|
202
|
|
|
$mapper->fetch($cookie); |
|
203
|
|
|
|
|
204
|
|
|
return $cookie; |
|
205
|
|
|
} |
|
206
|
|
|
|
|
207
|
|
|
|
|
208
|
|
|
public function discardCookie($userId, $series, $key) |
|
209
|
|
|
{ |
|
210
|
|
|
$identity = $this->retrieveIdenityByCookie($userId, $series, Entity\Identity::STATUS_ACTIVE); |
|
211
|
|
|
|
|
212
|
|
View Code Duplication |
if ($identity->getId() === null) { |
|
|
|
|
|
|
213
|
|
|
$this->logger->error('denial of service', [ |
|
214
|
|
|
'input' => [ |
|
215
|
|
|
'user' => $userId, |
|
216
|
|
|
'series' => $series, |
|
217
|
|
|
'key' => $key, |
|
218
|
|
|
], |
|
219
|
|
|
'account' => [ |
|
220
|
|
|
'user' => $identity->getUserId(), |
|
221
|
|
|
'identity' => $identity->getId(), |
|
222
|
|
|
], |
|
223
|
|
|
]); |
|
224
|
|
|
|
|
225
|
|
|
throw new DenialOfServiceAttempt; |
|
226
|
|
|
} |
|
227
|
|
|
|
|
228
|
|
|
$mapper = $this->mapperFactory->create(Mapper\CookieIdentity::class); |
|
229
|
|
|
|
|
230
|
|
View Code Duplication |
if ($identity->matchKey($key) === false) { |
|
|
|
|
|
|
231
|
|
|
$identity->setStatus(Entity\Identity::STATUS_BLOCKED); |
|
232
|
|
|
$mapper->store($identity); |
|
233
|
|
|
|
|
234
|
|
|
$this->logger->error('compromised cookie', [ |
|
235
|
|
|
'input' => [ |
|
236
|
|
|
'user' => $userId, |
|
237
|
|
|
'series' => $series, |
|
238
|
|
|
'key' => $key, |
|
239
|
|
|
], |
|
240
|
|
|
'account' => [ |
|
241
|
|
|
'user' => $identity->getUserId(), |
|
242
|
|
|
'identity' => $identity->getId(), |
|
243
|
|
|
], |
|
244
|
|
|
]); |
|
245
|
|
|
|
|
246
|
|
|
throw new CompromisedCookie; |
|
247
|
|
|
} |
|
248
|
|
|
|
|
249
|
|
|
$identity->setStatus(Entity\Identity::STATUS_DISCARDED); |
|
250
|
|
|
$mapper->store($identity); |
|
251
|
|
|
|
|
252
|
|
|
$this->logger->info('logout successful', [ |
|
253
|
|
|
'account' => [ |
|
254
|
|
|
'user' => $identity->getUserId(), |
|
255
|
|
|
'identity' => $identity->getId(), |
|
256
|
|
|
], |
|
257
|
|
|
]); |
|
258
|
|
|
|
|
259
|
|
|
} |
|
260
|
|
|
|
|
261
|
|
|
|
|
262
|
|
|
public function changeUserPassword($userId, $oldKey, $newKey) |
|
263
|
|
|
{ |
|
264
|
|
|
$list = $this->retrieveIdenitiesByUserId($userId, Entity\Identity::TYPE_PASSWORD); |
|
265
|
|
|
|
|
266
|
|
|
if (count($list) !== 1) { |
|
267
|
|
|
$this->logger->warning('acount not found', [ |
|
268
|
|
|
'input' => [ |
|
269
|
|
|
'user' => $userId, |
|
270
|
|
|
'old-key' => md5($oldKey), |
|
271
|
|
|
'new-key' => md5($newKey), |
|
272
|
|
|
], |
|
273
|
|
|
]); |
|
274
|
|
|
|
|
275
|
|
|
throw new IdentityNotFound; |
|
276
|
|
|
} |
|
277
|
|
|
|
|
278
|
|
|
$identity = $list->getLastEntity(); |
|
279
|
|
|
|
|
280
|
|
|
$mapper = $this->mapperFactory->create(Mapper\PasswordIdentity::class); |
|
281
|
|
|
|
|
282
|
|
View Code Duplication |
if ($identity->matchKey($oldKey) === false) { |
|
|
|
|
|
|
283
|
|
|
$this->logger->warning('wrong password', [ |
|
284
|
|
|
'input' => [ |
|
285
|
|
|
'user' => $userId, |
|
286
|
|
|
'old-key' => md5($oldKey), |
|
287
|
|
|
'new-key' => md5($newKey), |
|
288
|
|
|
], |
|
289
|
|
|
'account' => [ |
|
290
|
|
|
'user' => $identity->getUserId(), |
|
291
|
|
|
'identity' => $identity->getId(), |
|
292
|
|
|
], |
|
293
|
|
|
]); |
|
294
|
|
|
|
|
295
|
|
|
throw new PasswordNotMatch; |
|
296
|
|
|
} |
|
297
|
|
|
|
|
298
|
|
|
$identity->setKey($newKey); |
|
299
|
|
|
$mapper->store($identity); |
|
300
|
|
|
|
|
301
|
|
|
$this->discardAllUserCookies($identity->getUserId()); |
|
302
|
|
|
|
|
303
|
|
|
$this->logger->info('password changed', [ |
|
304
|
|
|
'account' => [ |
|
305
|
|
|
'user' => $identity->getUserId(), |
|
306
|
|
|
'identity' => $identity->getId(), |
|
307
|
|
|
], |
|
308
|
|
|
]); |
|
309
|
|
|
} |
|
310
|
|
|
|
|
311
|
|
|
|
|
312
|
|
|
public function getCurrentCookie() |
|
313
|
|
|
{ |
|
314
|
|
|
if (null === $this->currentCookie) { |
|
315
|
|
|
return new Entity\CookieIdentity; |
|
316
|
|
|
} |
|
317
|
|
|
|
|
318
|
|
|
return $this->currentCookie; |
|
319
|
|
|
} |
|
320
|
|
|
|
|
321
|
|
|
|
|
322
|
|
|
public function discardCurrentCookie() |
|
323
|
|
|
{ |
|
324
|
|
|
$cookie = new Entity\CookieIdentity; |
|
325
|
|
|
$cookie->setExpiresOn(time()); |
|
326
|
|
|
|
|
327
|
|
|
$this->currentCookie = $cookie; |
|
328
|
|
|
} |
|
329
|
|
|
} |
|
330
|
|
|
|
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.