1: <?php
2:
3: namespace Charcoal\User;
4:
5: use DateTime;
6: use DateTimeInterface;
7: use InvalidArgumentException;
8:
9:
10: use Charcoal\Model\AbstractModel;
11:
12:
13: use Charcoal\User\AuthTokenMetadata;
14:
15: 16: 17:
18: class AuthToken extends AbstractModel
19: {
20:
21: 22: 23:
24: private $ident;
25:
26: 27: 28:
29: private $token;
30:
31: 32: 33: 34:
35: private $username;
36:
37: 38: 39:
40: private $expiry;
41:
42: 43: 44: 45:
46: private $created;
47:
48: 49: 50: 51:
52: private $lastModified;
53:
54: 55: 56:
57: public function key()
58: {
59: return 'ident';
60: }
61:
62: 63: 64: 65:
66: public function setIdent($ident)
67: {
68: $this->ident = $ident;
69: return $this;
70: }
71:
72: 73: 74:
75: public function ident()
76: {
77: return $this->ident;
78: }
79:
80: 81: 82: 83:
84: public function setToken($token)
85: {
86: $this->token = $token;
87: return $this;
88: }
89:
90: 91: 92:
93: public function token()
94: {
95: return $this->token;
96: }
97:
98:
99: 100: 101: 102: 103: 104: 105:
106: public function setUsername($username)
107: {
108: if (!is_string($username)) {
109: throw new InvalidArgumentException(
110: 'Set user username: Username must be a string'
111: );
112: }
113: $this->username = mb_strtolower($username);
114: return $this;
115: }
116:
117: 118: 119:
120: public function username()
121: {
122: return $this->username;
123: }
124:
125: 126: 127: 128: 129:
130: public function setExpiry($expiry)
131: {
132: if ($expiry === null) {
133: $this->expiry = null;
134: return $this;
135: }
136: if (is_string($expiry)) {
137: $expiry = new DateTime($expiry);
138: }
139: if (!($expiry instanceof DateTimeInterface)) {
140: throw new InvalidArgumentException(
141: 'Invalid "Expiry" value. Must be a date/time string or a DateTime object.'
142: );
143: }
144: $this->expiry = $expiry;
145: return $this;
146: }
147:
148: 149: 150:
151: public function expiry()
152: {
153: return $this->expiry;
154: }
155:
156: 157: 158: 159: 160:
161: public function setCreated($created)
162: {
163: if ($created === null) {
164: $this->created = null;
165: return $this;
166: }
167: if (is_string($created)) {
168: $created = new DateTime($created);
169: }
170: if (!($created instanceof DateTimeInterface)) {
171: throw new InvalidArgumentException(
172: 'Invalid "Created" value. Must be a date/time string or a DateTime object.'
173: );
174: }
175: $this->created = $created;
176: return $this;
177: }
178:
179: 180: 181:
182: public function created()
183: {
184: return $this->created;
185: }
186:
187: 188: 189: 190: 191:
192: public function setLastModified($lastModified)
193: {
194: if ($lastModified === null) {
195: $this->lastModified = null;
196: return $this;
197: }
198: if (is_string($lastModified)) {
199: $lastModified = new DateTime($lastModified);
200: }
201: if (!($lastModified instanceof DateTimeInterface)) {
202: throw new InvalidArgumentException(
203: 'Invalid "Last Modified" value. Must be a date/time string or a DateTime object.'
204: );
205: }
206: $this->lastModified = $lastModified;
207: return $this;
208: }
209:
210: 211: 212:
213: public function lastModified()
214: {
215: return $this->lastModified;
216: }
217:
218: 219: 220: 221: 222: 223:
224: public function generate($username)
225: {
226: $this->setIdent(bin2hex(random_bytes(16)));
227: $this->setToken(bin2hex(random_bytes(32)));
228: $this->setUsername($username);
229: $this->setExpiry('now + '.$this->metadata()->cookieDuration());
230:
231: return $this;
232: }
233:
234: 235: 236:
237: public function sendCookie()
238: {
239: $cookieName = $this->metadata()->cookieName();
240: $value = $this->ident().';'.$this->token();
241: $expiry = $this->expiry()->getTimestamp();
242: $secure = $this->metadata()->httpsOnly();
243:
244: setcookie($cookieName, $value, $expiry, '', '', $secure);
245:
246: return $this;
247: }
248:
249: 250: 251: 252:
253: public function preSave()
254: {
255: parent::preSave();
256:
257: if (password_needs_rehash($this->token, PASSWORD_DEFAULT)) {
258: $this->token = password_hash($this->token, PASSWORD_DEFAULT);
259: }
260: $this->setCreated('now');
261: $this->setLastModified('now');
262:
263: return true;
264: }
265:
266: 267: 268: 269: 270:
271: public function preUpdate(array $properties = null)
272: {
273: parent::preUpdate($properties);
274:
275: $this->setLastModified('now');
276:
277: return true;
278: }
279:
280: 281: 282:
283: public function getTokenDataFromCookie()
284: {
285: $cookieName = $this->metadata()->cookieName();
286:
287: if (!isset($_COOKIE[$cookieName])) {
288: return null;
289: }
290:
291: $authCookie = $_COOKIE[$cookieName];
292: $vals = explode(';', $authCookie);
293: if (!isset($vals[0]) || !isset($vals[1])) {
294: return null;
295: }
296:
297: return [
298: 'ident' => $vals[0],
299: 'token' => $vals[1]
300: ];
301: }
302:
303: 304: 305: 306: 307:
308: public function getUserId($ident, $token)
309: {
310: return $this->getUsernameFromToken($ident, $token);
311: }
312:
313: 314: 315: 316: 317:
318: public function getUsernameFromToken($ident, $token)
319: {
320: $this->load($ident);
321: if (!$this->ident()) {
322: $this->logger->warning(sprintf('Auth token not found: "%s"', $ident));
323: return '';
324: }
325:
326:
327: $now = new DateTime('now');
328: if (!$this->expiry() || $now > $this->expiry()) {
329: $this->logger->warning('Expired auth token');
330: $this->delete();
331: return '';
332: }
333:
334:
335: if (password_verify($token, $this->token()) !== true) {
336: $this->panic();
337: $this->delete();
338: return '';
339: }
340:
341:
342: return $this->username();
343: }
344:
345: 346: 347: 348: 349:
350: protected function panic()
351: {
352: $this->logger->error(
353: 'Possible security breach: an authentication token was found in the database but its token does not match.'
354: );
355:
356: if ($this->username) {
357: $table = $this->source()->table();
358: $q = '
359: delete from
360: '.$table.'
361: where
362: username = :username';
363: $this->source()->dbQuery($q, [
364: 'username'=>$this->username()
365: ]);
366: }
367: }
368:
369: 370: 371: 372: 373: 374:
375: protected function createMetadata(array $data = null)
376: {
377: $metadata = new AuthTokenMetadata();
378: if ($data !== null) {
379: $metadata->setData($data);
380: }
381: return $metadata;
382: }
383: }
384: