Overview

Namespaces

  • Charcoal
    • Object
    • User
      • Acl

Classes

  • Charcoal\Object\Content
  • Charcoal\Object\ObjectRevision
  • Charcoal\Object\ObjectRoute
  • Charcoal\Object\ObjectSchedule
  • Charcoal\Object\UserData
  • Charcoal\User\AbstractUser
  • Charcoal\User\Acl\Manager
  • Charcoal\User\Acl\Permission
  • Charcoal\User\Acl\PermissionCategory
  • Charcoal\User\Acl\Role
  • Charcoal\User\Authenticator
  • Charcoal\User\Authorizer
  • Charcoal\User\AuthToken
  • Charcoal\User\AuthTokenMetadata
  • Charcoal\User\GenericUser

Interfaces

  • Charcoal\Object\ArchivableInterface
  • Charcoal\Object\CategorizableInterface
  • Charcoal\Object\CategorizableMultipleInterface
  • Charcoal\Object\CategoryInterface
  • Charcoal\Object\ContentInterface
  • Charcoal\Object\HierarchicalInterface
  • Charcoal\Object\ObjectRevisionInterface
  • Charcoal\Object\ObjectRouteInterface
  • Charcoal\Object\ObjectScheduleInterface
  • Charcoal\Object\PublishableInterface
  • Charcoal\Object\RevisionableInterface
  • Charcoal\Object\RoutableInterface
  • Charcoal\Object\UserDataInterface
  • Charcoal\User\UserInterface

Traits

  • Charcoal\Object\ArchivableTrait
  • Charcoal\Object\CategorizableMultipleTrait
  • Charcoal\Object\CategorizableTrait
  • Charcoal\Object\CategoryTrait
  • Charcoal\Object\HierarchicalTrait
  • Charcoal\Object\PublishableTrait
  • Charcoal\Object\RevisionableTrait
  • Charcoal\Object\RoutableTrait
  • Overview
  • Namespace
  • Class
  1: <?php
  2: 
  3: namespace Charcoal\User;
  4: 
  5: use DateTime;
  6: use DateTimeInterface;
  7: use InvalidArgumentException;
  8: 
  9: // Dependency from 'charcoal-core'
 10: use Charcoal\Model\AbstractModel;
 11: 
 12: // Local depdendency
 13: use Charcoal\User\AuthTokenMetadata;
 14: 
 15: /**
 16:  * Authorization token; to keep a user logged in
 17:  */
 18: class AuthToken extends AbstractModel
 19: {
 20: 
 21:     /**
 22:      * @var string
 23:      */
 24:     private $ident;
 25: 
 26:     /**
 27:      * @var string
 28:      */
 29:     private $token;
 30: 
 31:     /**
 32:      * The username should be unique and mandatory.
 33:      * @var string
 34:      */
 35:     private $username;
 36: 
 37:     /**
 38:      * @var DatetimeInterface
 39:      */
 40:     private $expiry;
 41: 
 42:     /**
 43:      * Token creation date (set automatically on save)
 44:      * @var DateTimeInterface
 45:      */
 46:     private $created;
 47: 
 48:     /**
 49:      * Token last modified date (set automatically on save and update)
 50:      * @var DateTimeInterface
 51:      */
 52:     private $lastModified;
 53: 
 54:     /**
 55:      * @return string
 56:      */
 57:     public function key()
 58:     {
 59:         return 'ident';
 60:     }
 61: 
 62:     /**
 63:      * @param string $ident The token ident.
 64:      * @return AuthToken Chainable
 65:      */
 66:     public function setIdent($ident)
 67:     {
 68:         $this->ident = $ident;
 69:         return $this;
 70:     }
 71: 
 72:     /**
 73:      * @return string
 74:      */
 75:     public function ident()
 76:     {
 77:         return $this->ident;
 78:     }
 79: 
 80:     /**
 81:      * @param string $token The token.
 82:      * @return AuthToken Chainable
 83:      */
 84:     public function setToken($token)
 85:     {
 86:         $this->token = $token;
 87:         return $this;
 88:     }
 89: 
 90:     /**
 91:      * @return string
 92:      */
 93:     public function token()
 94:     {
 95:         return $this->token;
 96:     }
 97: 
 98: 
 99:     /**
100:      * Force a lowercase username
101:      *
102:      * @param string $username The username (also the login name).
103:      * @throws InvalidArgumentException If the username is not a string.
104:      * @return User Chainable
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:      * @return string
119:      */
120:     public function username()
121:     {
122:         return $this->username;
123:     }
124: 
125:     /**
126:      * @param DateTime|string|null $expiry The date/time at object's creation.
127:      * @throws InvalidArgumentException If the date/time is invalid.
128:      * @return Content Chainable
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:      * @return DateTimeInterface|null
150:      */
151:     public function expiry()
152:     {
153:         return $this->expiry;
154:     }
155: 
156:     /**
157:      * @param DateTime|string|null $created The date/time at object's creation.
158:      * @throws InvalidArgumentException If the date/time is invalid.
159:      * @return Content Chainable
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:      * @return DateTime|null
181:      */
182:     public function created()
183:     {
184:         return $this->created;
185:     }
186: 
187:     /**
188:      * @param DateTime|string|null $lastModified The last modified date/time.
189:      * @throws InvalidArgumentException If the date/time is invalid.
190:      * @return Content Chainable
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:      * @return DateTime
212:      */
213:     public function lastModified()
214:     {
215:         return $this->lastModified;
216:     }
217: 
218:     /**
219:      * Note: the `random_bytes()` function is new to PHP-7. Available in PHP 5 with `compat-random`.
220:      *
221:      * @param string $username The username to generate the auth token from.
222:      * @return AuthToken Chainable
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:      * @return AuthToken Chainable
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:       * StorableTrait > preSave(): Called automatically before saving the object to source.
251:       * @return boolean
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:      * StorableTrait > preUpdate(): Called automatically before updating the object to source.
268:      * @param array $properties The properties (ident) set for update.
269:      * @return boolean
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:      * @return array `['ident'=>'', 'token'=>'']
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:      * @param mixed  $ident The auth-token identifier.
305:      * @param string $token The token key to validate against.
306:      * @return mixed The user id.
307:      */
308:     public function getUserId($ident, $token)
309:     {
310:         return $this->getUsernameFromToken($ident, $token);
311:     }
312: 
313:     /**
314:      * @param mixed  $ident The auth-token identifier (username).
315:      * @param string $token The token to validate against.
316:      * @return mixed The user id. An empty string if no token match.
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:         // Expired cookie
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:         // Validate encrypted token
335:         if (password_verify($token, $this->token()) !== true) {
336:             $this->panic();
337:             $this->delete();
338:             return '';
339:         }
340: 
341:         // Success!
342:         return $this->username();
343:     }
344: 
345:     /**
346:      * Something is seriously wrong: a cookie ident was in the database but with a tampered token.
347:      *
348:      * @return void
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:      * DescribableTrait > create_metadata().
371:      *
372:      * @param array $data Optional data to intialize the Metadata object with.
373:      * @return MetadataInterface
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: 
API documentation generated by ApiGen