Skip to content

Commit f4ed22e

Browse files
committed
Put sessions in the database instead of memcached
1 parent 8570f36 commit f4ed22e

File tree

1 file changed

+131
-39
lines changed

1 file changed

+131
-39
lines changed

src/libraries/Authentication.php

+131-39
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,24 @@
66
use \BNETDocs\Libraries\User;
77

88
use \CarlBennett\MVC\Libraries\Common;
9+
use \CarlBennett\MVC\Libraries\DatabaseDriver;
910

11+
use \DateTime;
12+
use \DateTimeZone;
13+
use \InvalidArgumentException;
1014
use \PDO;
1115
use \PDOException;
12-
use \UnexpectedValueException;
1316

1417
/**
1518
* Authentication
1619
* The class that handles authenticating and verifying a client.
1720
*/
1821
class Authentication {
1922

20-
const CACHE_KEY = 'bnetdocs-auth-%s';
21-
const COOKIE_NAME = 'sid';
22-
const TTL = 2592000; // 1 month
23+
const COOKIE_NAME = 'sid';
24+
const DATE_SQL = 'Y-m-d H:i:s';
25+
const MAX_USER_AGENT = 255;
26+
const TTL = 2592000; // 1 month
2327

2428
/**
2529
* @var string $key
@@ -47,8 +51,26 @@ private function __construct() {}
4751
*
4852
* @return bool Indicates if the operation succeeded.
4953
*/
50-
protected static function discard( $key ) {
51-
return Common::$cache->delete( sprintf( self::CACHE_KEY, $key ));
54+
protected static function discard(string $key) {
55+
if (!isset(Common::$database)) {
56+
Common::$database = DatabaseDriver::getDatabaseObject();
57+
}
58+
59+
try {
60+
$stmt = Common::$database->prepare('
61+
DELETE FROM `user_sessions` WHERE `id` = :id LIMIT 1;
62+
');
63+
64+
$stmt->bindParam(':id', $key, PDO::PARAM_STR);
65+
66+
$r = $stmt->execute();
67+
$stmt->closeCursor();
68+
69+
} catch (PDOException $e) {
70+
throw new QueryException('Cannot delete user session key', $e);
71+
} finally {
72+
return $r;
73+
}
5274
}
5375

5476
/**
@@ -59,13 +81,13 @@ protected static function discard( $key ) {
5981
*
6082
* @return array The fingerprint details.
6183
*/
62-
protected static function getFingerprint( User &$user ) {
84+
protected static function getFingerprint(User &$user) {
6385
$fingerprint = array();
6486

65-
$fingerprint['ip_address'] = getenv( 'REMOTE_ADDR' );
66-
67-
$fingerprint['user_id'] = (
68-
isset( self::$user ) ? self::$user->getId() : null
87+
$fingerprint['ip_address'] = getenv('REMOTE_ADDR');
88+
$fingerprint['user_id'] = (is_null($user) ? null : $user->getId());
89+
$fingerprint['user_agent'] = substr(
90+
getenv('HTTP_USER_AGENT'), 0, self::MAX_USER_AGENT
6991
);
7092

7193
return $fingerprint;
@@ -79,9 +101,12 @@ protected static function getFingerprint( User &$user ) {
79101
*
80102
* @return string The unique string.
81103
*/
82-
protected static function getUniqueKey( User &$user ) {
104+
protected static function getUniqueKey(User &$user) {
105+
if (!$user instanceof User) {
106+
throw new InvalidArgumentException('$user is not instance of User');
107+
}
83108
return hash( 'sha1',
84-
mt_rand() . getenv( 'REMOTE_ADDR' ) .
109+
mt_rand() . getenv('REMOTE_ADDR') .
85110
$user->getId() . $user->getEmail() . $user->getUsername() .
86111
$user->getPasswordHash() . $user->getPasswordSalt() .
87112
Common::$config->bnetdocs->user_password_pepper
@@ -97,16 +122,16 @@ protected static function getUniqueKey( User &$user ) {
97122
*
98123
* @return bool Indicates if the browser cookie was sent.
99124
*/
100-
public static function login( User &$user ) {
101-
if ( !$user instanceof User ) {
102-
throw new UnexpectedValueException( '$user is not instance of User' );
125+
public static function login(User &$user) {
126+
if (!$user instanceof User) {
127+
throw new InvalidArgumentException('$user is not instance of User');
103128
}
104129

105-
self::$key = self::getUniqueKey( $user );
130+
self::$key = self::getUniqueKey($user);
106131
self::$user = $user;
107132

108-
$fingerprint = self::getFingerprint( $user );
109-
self::store( self::$key, $fingerprint );
133+
$fingerprint = self::getFingerprint($user);
134+
self::store(self::$key, $fingerprint);
110135

111136
// 'domain' is an empty string to only allow this specific http host to
112137
// authenticate, excluding any subdomains. If we were to specify our
@@ -130,7 +155,7 @@ public static function login( User &$user ) {
130155
* @return bool Indicates if the browser cookie was sent.
131156
*/
132157
public static function logout() {
133-
self::discard( self::$key );
158+
self::discard(self::$key);
134159

135160
self::$key = '';
136161
self::$user = null;
@@ -156,31 +181,92 @@ public static function logout() {
156181
*
157182
* @param string $key The secret key, typically from the client.
158183
*
159-
* @return string The fingerprint details, or false if not found.
184+
* @return array The fingerprint details, or false if not found.
160185
*/
161-
protected static function lookup( $key ) {
162-
$fingerprint = Common::$cache->get( sprintf( self::CACHE_KEY, $key ));
163-
164-
if ( $fingerprint !== false ) {
165-
$fingerprint = unserialize( $fingerprint );
186+
protected static function lookup(string $key) {
187+
if (!isset(Common::$database)) {
188+
Common::$database = DatabaseDriver::getDatabaseObject();
166189
}
167190

168-
return $fingerprint;
191+
$fingerprint = false;
192+
193+
try {
194+
$stmt = Common::$database->prepare('
195+
SELECT `user_id`, `ip_address`, `user_agent`
196+
FROM `user_sessions` WHERE `id` = :id LIMIT 1;
197+
');
198+
199+
$stmt->bindParam(':id', $key, PDO::PARAM_STR);
200+
201+
$r = $stmt->execute();
202+
203+
if ($r) {
204+
$fingerprint = $stmt->fetch(PDO::FETCH_ASSOC);
205+
}
206+
207+
$stmt->closeCursor();
208+
209+
} catch (PDOException $e) {
210+
throw new QueryException('Cannot lookup user session key', $e);
211+
} finally {
212+
return $fingerprint;
213+
}
169214
}
170215

171216
/**
172217
* store()
173218
* Stores authentication info server-side for lookup later.
174219
*
175-
* @param string $key The secret key.
176-
* @param string $value The fingerprint details.
220+
* @param string $key The secret key.
221+
* @param array $fingerprint The fingerprint details.
177222
*
178223
* @return bool Indicates if the operation succeeded.
179224
*/
180-
protected static function store( $key, &$fingerprint ) {
181-
return Common::$cache->set(
182-
sprintf( self::CACHE_KEY, $key ), serialize( $fingerprint ), self::TTL
225+
protected static function store(string $key, array &$fingerprint) {
226+
if (!isset(Common::$database)) {
227+
Common::$database = DatabaseDriver::getDatabaseObject();
228+
}
229+
230+
$tz = new DateTimeZone('Etc/UTC');
231+
232+
$user_id = $fingerprint['user_id'];
233+
$ip_address = $fingerprint['ip_address'];
234+
$user_agent = $fingerprint['user_agent'];
235+
$created_dt = new DateTime('now', $tz);
236+
$created_str = $created_dt->format(self::DATE_SQL);
237+
$expires_dt = new DateTime(
238+
'@' . ($created_dt->getTimestamp() + self::TTL), $tz
183239
);
240+
$expires_str = $expires_dt->format(self::DATE_SQL);
241+
242+
$r = false;
243+
244+
try {
245+
$stmt = Common::$database->prepare('
246+
INSERT INTO `user_sessions` (
247+
`id`, `user_id`, `ip_address`, `user_agent`,
248+
`created_datetime`, `expires_datetime`
249+
) VALUES (
250+
:id, :user_id, :ip_address, :user_agent,
251+
:created_dt, :expires_dt
252+
);
253+
');
254+
255+
$stmt->bindParam(':id', $key, PDO::PARAM_STR);
256+
$stmt->bindParam(':user_id', $user_id, PDO::PARAM_INT);
257+
$stmt->bindParam(':ip_address', $ip_address, PDO::PARAM_STR);
258+
$stmt->bindParam(':user_agent', $user_agent, PDO::PARAM_STR);
259+
$stmt->bindParam(':created_dt', $created_str, PDO::PARAM_STR);
260+
$stmt->bindParam(':expires_dt', $expires_str, PDO::PARAM_STR);
261+
262+
$r = $stmt->execute();
263+
$stmt->closeCursor();
264+
265+
} catch (PDOException $e) {
266+
throw new QueryException('Cannot store user session key', $e);
267+
} finally {
268+
return $r;
269+
}
184270
}
185271

186272
/**
@@ -192,33 +278,39 @@ protected static function store( $key, &$fingerprint ) {
192278
public static function verify() {
193279
// get client's lookup key
194280
self::$key = (
195-
isset( $_COOKIE[self::COOKIE_NAME] ) ? $_COOKIE[self::COOKIE_NAME] : ''
281+
isset($_COOKIE[self::COOKIE_NAME]) ? $_COOKIE[self::COOKIE_NAME] : ''
196282
);
197283

198284
// no user yet
199285
self::$user = null;
200286

201287
// return if cookie is empty or not set
202-
if ( empty( self::$key )) { return false; }
288+
if (empty(self::$key)) { return false; }
203289

204290
// lookup key in our store
205-
$lookup = self::lookup( self::$key );
291+
$lookup = self::lookup(self::$key);
206292

207293
// logout and return if we could not verify their info
208-
if ( !$lookup ) {
294+
if (!$lookup) {
209295
self::logout();
210296
return false;
211297
}
212298

213299
// logout and return if their fingerprint ip address does not match
214-
if ( $lookup['ip_address'] !== getenv( 'REMOTE_ADDR' )) {
300+
if ($lookup['ip_address'] !== getenv('REMOTE_ADDR')) {
301+
self::logout();
302+
return false;
303+
}
304+
305+
// logout and return if their fingerprint user agent does not match
306+
if ($lookup['user_agent'] !== getenv('HTTP_USER_AGENT')) {
215307
self::logout();
216308
return false;
217309
}
218310

219311
// verified info, let's get the user object
220-
if ( isset( $lookup['user_id'] )) {
221-
self::$user = new User( $lookup['user_id'] );
312+
if (isset($lookup['user_id'])) {
313+
self::$user = new User($lookup['user_id']);
222314
}
223315

224316
return true;

0 commit comments

Comments
 (0)