66use \BNETDocs \Libraries \User ;
77
88use \CarlBennett \MVC \Libraries \Common ;
9+ use \CarlBennett \MVC \Libraries \DatabaseDriver ;
910
11+ use \DateTime ;
12+ use \DateTimeZone ;
13+ use \InvalidArgumentException ;
1014use \PDO ;
1115use \PDOException ;
12- use \UnexpectedValueException ;
1316
1417/**
1518 * Authentication
1619 * The class that handles authenticating and verifying a client.
1720 */
1821class 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