From 1ee817f6e97b28f95f7ab3388736bc49336f5ac1 Mon Sep 17 00:00:00 2001 From: jaleonardo <32501234+apps-caraga@users.noreply.github.com> Date: Wed, 14 Sep 2022 17:17:10 +0800 Subject: [PATCH 1/7] Registration w/ addl data Updated registration endpoint for dbAuth to accomodate other posted registration data. Also added some new dbAuth properties 1. usernameMinLength : specify minimum length of username (5) 2. usernameMaxLength : specify maximum length of username (40) 3. usernamePattern : specify regex pattern for usernames ('/^[A-Za-z0-9]+$/') // defaults to alphanumeric chars only --- .../Middleware/DbAuthMiddleware.php | 37 +++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/src/Tqdev/PhpCrudApi/Middleware/DbAuthMiddleware.php b/src/Tqdev/PhpCrudApi/Middleware/DbAuthMiddleware.php index 0cc388e4..5bbbf12c 100644 --- a/src/Tqdev/PhpCrudApi/Middleware/DbAuthMiddleware.php +++ b/src/Tqdev/PhpCrudApi/Middleware/DbAuthMiddleware.php @@ -59,6 +59,15 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $usernameColumnName = $this->getProperty('usernameColumn', 'username'); $usernameColumn = $table->getColumn($usernameColumnName); $passwordColumnName = $this->getProperty('passwordColumn', 'password'); + $usernamePattern = $this->getProperty('usernamePattern','/^[A-Za-z0-9]+$/'); // specify regex pattern for username, defaults to alphanumeric characters + $usernameMinLength = (int)$this->getProperty('usernameMinLength',5); + $usernameMaxLength = (int)$this->getProperty('usernameMaxLength',30); + if($usernameMinLength > $usernameMaxLength){ + //obviously, $usernameMinLength should be less than $usernameMaxLength, but we'll still check in case of mis-config then we'll swap the 2 values + $lesser = $usernameMaxLength; + $usernameMaxLength = $usernameMinLength; + $usernameMinLength = $lesser; + } $passwordLength = $this->getProperty('passwordLength', '12'); $pkName = $table->getPk()->getName(); $registerUser = $this->getProperty('registerUser', ''); @@ -79,14 +88,36 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface if (strlen($password) < $passwordLength) { return $this->responder->error(ErrorCode::PASSWORD_TOO_SHORT, $passwordLength); } + if(strlen($username) < $usernameMinLength){ + return $this->responder->error(ErrorCode::INPUT_VALIDATION_FAILED, $username . " [ Username length must be at least ". $usernameMinLength ." characters.]"); + } + if(strlen($username) > $usernameMaxLength){ + return $this->responder->error(ErrorCode::INPUT_VALIDATION_FAILED, $username . " [ Username length must not exceed ". $usernameMaxLength ." characters.]"); + } + if(!preg_match($usernamePattern, $username)){ + return $this->responder->error(ErrorCode::INPUT_VALIDATION_FAILED, $username . " [ Username contains disallowed characters.]"); + } $users = $this->db->selectAll($table, $columnNames, $condition, $columnOrdering, 0, 1); if (!empty($users)) { return $this->responder->error(ErrorCode::USER_ALREADY_EXIST, $username); } $data = json_decode($registerUser, true); - $data = is_array($data) ? $data : []; - $data[$usernameColumnName] = $username; - $data[$passwordColumnName] = password_hash($password, PASSWORD_DEFAULT); + $data = is_array($data) ? $data : (array)$body; + // get the original posted data + $userTableColumns = $table->getColumnNames(); + foreach($data as $key=>$value){ + if(in_array($key,$userTableColumns)){ + // process only posted data if the key exists as users table column + if($key === $usernameColumnName){ + $data[$usernameColumnName] = $username; //process the username and password as usual + }else if($key === $passwordColumnName){ + $data[$passwordColumnName] = password_hash($password, PASSWORD_DEFAULT); + }else{ + $data[$key] = filter_var($value, FILTER_VALIDATE_EMAIL) ? $value : filter_var($value,FILTER_SANITIZE_ENCODED); + //sanitize all other inputs, except for valid or properly formatted email address + } + } + } $this->db->createSingle($table, $data); $users = $this->db->selectAll($table, $columnNames, $condition, $columnOrdering, 0, 1); foreach ($users as $user) { From 0f4e2b9c6c0d1cbce4582ed0977f8dfe6588938a Mon Sep 17 00:00:00 2001 From: jaleonardo <32501234+apps-caraga@users.noreply.github.com> Date: Fri, 16 Sep 2022 05:15:24 +0800 Subject: [PATCH 2/7] Minor formatting update Formatted section on setting up JWT authentication to make the list more readable --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 623cf780..f6831c1d 100644 --- a/README.md +++ b/README.md @@ -902,10 +902,10 @@ Add a web application to this project and grab the code snippet for later use. Then you have to configure the `jwtAuth.secrets` configuration in your `api.php` file. This can be done as follows: -a. Log a user in to your Firebase-based app, get an authentication token for that user -b. Go to [https://jwt.io/](https://jwt.io/) and paste the token in the decoding field -c. Read the decoded header information from the token, it will give you the correct `kid` -d. Grab the public key via this [URL](https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com), which corresponds to your `kid` from previous step +a. Log a user in to your Firebase-based app, get an authentication token for that user +b. Go to [https://jwt.io/](https://jwt.io/) and paste the token in the decoding field +c. Read the decoded header information from the token, it will give you the correct `kid` +d. Grab the public key via this [URL](https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com), which corresponds to your `kid` from previous step e. Now, just fill `jwtAuth.secrets` with your public key in the `api.php` Here is an example of what it should look like in the configuration: @@ -965,7 +965,7 @@ and define a 'authorization.tableHandler' function that returns 'false' for thes }, The above example will restrict access to the table 'license_keys' for all operations. - + 'authorization.columnHandler' => function ($operation, $tableName, $columnName) { return !($tableName == 'users' && $columnName == 'password'); }, From 57a761ff519ab0f137aeee255c1e52e6439b3582 Mon Sep 17 00:00:00 2001 From: jaleonardo <32501234+apps-caraga@users.noreply.github.com> Date: Fri, 16 Sep 2022 08:06:09 +0800 Subject: [PATCH 3/7] Updated readme (Add'l dbAuth properties) Added the dbAuth properties for checking minimum and maximum length of username as well as allowed characters. --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index f6831c1d..7ca2410c 100644 --- a/README.md +++ b/README.md @@ -703,6 +703,9 @@ You can tune the middleware behavior using middleware specific configuration par - "dbAuth.passwordColumn": The users table column that holds passwords ("password") - "dbAuth.returnedColumns": The columns returned on successful login, empty means 'all' ("") - "dbAuth.usernameFormField": The name of the form field that holds the username ("username") +- "dbAuth.usernamePattern": Specify regex pattern for username. Defaults to alpha-numeric charactes ("/^[A-Za-z0-9]+$/") +- "dbAuth.usernameMaxLength": Specify maximum length of username (30) +- "dbAuth.usernameMinLength": Specify minimum length of username (5) - "dbAuth.passwordFormField": The name of the form field that holds the password ("password") - "dbAuth.newPasswordFormField": The name of the form field that holds the new password ("newPassword") - "dbAuth.registerUser": JSON user data (or "1") in case you want the /register endpoint enabled ("") From d5c63a6b1ecdb15025c29e04f21a75cca2d3222c Mon Sep 17 00:00:00 2001 From: jaleonardo <32501234+apps-caraga@users.noreply.github.com> Date: Sat, 17 Sep 2022 21:51:12 +0800 Subject: [PATCH 4/7] Added workaround for duplicate key error Since we're processing additional data during registration, we need to check if these data were defined in db to be unique. For example, email addresses are usually used just once in an application. We can query the database to check if the new email address is not yet registered, but, in some cases, we may more than 2 or 3 or more unique fields (not common, but possible), hence we would also need to query 2,3 or more times. As a TEMPORARY WORKAROUND, we'll just attempt to register the new user and wait for the db to throw a DUPLICATE KEY EXCEPTION. --- .../PhpCrudApi/Middleware/DbAuthMiddleware.php | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Tqdev/PhpCrudApi/Middleware/DbAuthMiddleware.php b/src/Tqdev/PhpCrudApi/Middleware/DbAuthMiddleware.php index 5bbbf12c..2f828ecb 100644 --- a/src/Tqdev/PhpCrudApi/Middleware/DbAuthMiddleware.php +++ b/src/Tqdev/PhpCrudApi/Middleware/DbAuthMiddleware.php @@ -118,7 +118,21 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface } } } - $this->db->createSingle($table, $data); + try{ + $this->db->createSingle($table, $data); + /* Since we're processing additional data during registration, we need to check if these data were defined in db to be unique. + * For example, emailAddress are usually used just once in an application. We can query the database to check if the new emailAddress is not yet registered, + * but, in some cases, we may more than 2 or 3 or more unique fields (not common, but possible), hence we would also need to + * query 2,3 or more times. + * As a TEMPORARY WORKAROUND, we'll just attempt to register the new user and wait for the db to throw a DUPLICATE KEY EXCEPTION. + */ + }catch(\PDOException error){ + if($error->getCode() ==="23000"){ + return $this->responder->error(ErrorCode::DUPLICATE_KEY_EXCEPTION,'',$error->getMessage()); + }else{ + return $this->responder->error(ErrorCode::INPUT_VALIDATION_FAILED,$$error->getMessage()); + } + } $users = $this->db->selectAll($table, $columnNames, $condition, $columnOrdering, 0, 1); foreach ($users as $user) { unset($user[$passwordColumnName]); From c86eaf363c18b10ff31ff94b4039ffe520da1f24 Mon Sep 17 00:00:00 2001 From: jaleonardo Date: Mon, 6 Mar 2023 11:22:01 +0800 Subject: [PATCH 5/7] Refresh session data via /me endpoint --- README.md | 1 + .../Middleware/DbAuthMiddleware.php | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/README.md b/README.md index b2483de7..00350cb3 100644 --- a/README.md +++ b/README.md @@ -708,6 +708,7 @@ You can tune the middleware behavior using middleware specific configuration par - "dbAuth.usernameColumn": The users table column that holds usernames ("username") - "dbAuth.passwordColumn": The users table column that holds passwords ("password") - "dbAuth.returnedColumns": The columns returned on successful login, empty means 'all' ("") +- "dbAuth.refreshSession": Number of minutes before a session is refreshed via api.php/me endpoint, (0) - "dbAuth.usernameFormField": The name of the form field that holds the username ("username") - "dbAuth.usernamePattern": Specify regex pattern for username. Defaults to alpha-numeric charactes ("/^[A-Za-z0-9]+$/") - "dbAuth.usernameMaxLength": Specify maximum length of username (30) diff --git a/src/Tqdev/PhpCrudApi/Middleware/DbAuthMiddleware.php b/src/Tqdev/PhpCrudApi/Middleware/DbAuthMiddleware.php index 3657dd9a..2a136301 100644 --- a/src/Tqdev/PhpCrudApi/Middleware/DbAuthMiddleware.php +++ b/src/Tqdev/PhpCrudApi/Middleware/DbAuthMiddleware.php @@ -156,6 +156,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface session_regenerate_id(true); } unset($user[$passwordColumnName]); + $_SESSION['updatedAt'] = time(); $_SESSION['user'] = $user; return $this->responder->success($user); } else { @@ -173,6 +174,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface session_regenerate_id(true); } unset($user[$passwordColumnName]); + $_SESSION['updatedAt'] = time(); $_SESSION['user'] = $user; return $this->responder->success($user); } @@ -221,6 +223,25 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface } if ($method == 'GET' && $path == 'me') { if (isset($_SESSION['user'])) { + $updateAfter = $this->getProperty('refreshSession',0) * 60;//update session after x minutes + if($updateAfter > 0 &&( time() >($_SESSION['user']['updatedAt'] + $updateAfter))){ + $tableName = $this->getProperty('loginTable','users'); + $table = $this->reflection->getTable($tableName); + $pkName = $table->getPk()->getName(); + $passwordColumnName = $this->getProperty('passwordColumn',''); + $returnedColumns = $this->getProperty('returnedColumns',''); + if(!$returnedColumns){ + $columnNames = $table->getColumnNames(); + }else{ + $columnNames = array_map)('trim',explode(',',$returnedColumns)); + $columnNames[] = $passwordColumnName; + $columnNames = array_values(array_unique($columnNames)); + } + $user = $this->db->selectSingle($table,$columnNames,$_SESSION['user'][$pkName]); + unset($user[$passwordColumnName]); + $user['updatedAt'] = time(); + $_SESSION['user'] = $user; + } return $this->responder->success($_SESSION['user']); } return $this->responder->error(ErrorCode::AUTHENTICATION_REQUIRED, ''); From 17e30fe33e8f9dd19082a7ceef49b978a941b898 Mon Sep 17 00:00:00 2001 From: jaleonardo <32501234+apps-caraga@users.noreply.github.com> Date: Mon, 6 Mar 2023 13:38:43 +0800 Subject: [PATCH 6/7] Option to update session data via /me end-point Changes $_SESSION['user']['updatedAt'] - set to time when the session was created/updated dbAuth.refreshSession - number of minutes after which the session data is refreshed when the /me end-point is called --- src/Tqdev/PhpCrudApi/Middleware/DbAuthMiddleware.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tqdev/PhpCrudApi/Middleware/DbAuthMiddleware.php b/src/Tqdev/PhpCrudApi/Middleware/DbAuthMiddleware.php index 2a136301..c1499ba8 100644 --- a/src/Tqdev/PhpCrudApi/Middleware/DbAuthMiddleware.php +++ b/src/Tqdev/PhpCrudApi/Middleware/DbAuthMiddleware.php @@ -233,7 +233,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface if(!$returnedColumns){ $columnNames = $table->getColumnNames(); }else{ - $columnNames = array_map)('trim',explode(',',$returnedColumns)); + $columnNames = array_map('trim',explode(',',$returnedColumns)); $columnNames[] = $passwordColumnName; $columnNames = array_values(array_unique($columnNames)); } From a72fd8dc0b44d2b07f28156e5abf67edc5986ab1 Mon Sep 17 00:00:00 2001 From: jaleonardo <32501234+apps-caraga@users.noreply.github.com> Date: Thu, 6 Apr 2023 10:35:55 +0800 Subject: [PATCH 7/7] Update DbAuthMiddleware.php Changes $usernamePattern - defaults to /^\p{L}+$/u , visible characters, no punctuation or numbers, unicode mode $usernameMaxLength - defaults to 255 changed validation of other inputs from filter_validate() to htmlspecialchars() fixed typos missing and extra $ --- src/Tqdev/PhpCrudApi/Middleware/DbAuthMiddleware.php | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Tqdev/PhpCrudApi/Middleware/DbAuthMiddleware.php b/src/Tqdev/PhpCrudApi/Middleware/DbAuthMiddleware.php index c1499ba8..8d9dad3b 100644 --- a/src/Tqdev/PhpCrudApi/Middleware/DbAuthMiddleware.php +++ b/src/Tqdev/PhpCrudApi/Middleware/DbAuthMiddleware.php @@ -71,9 +71,9 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $usernameColumnName = $this->getProperty('usernameColumn', 'username'); $usernameColumn = $table->getColumn($usernameColumnName); $passwordColumnName = $this->getProperty('passwordColumn', 'password'); - $usernamePattern = $this->getProperty('usernamePattern','/^[A-Za-z0-9]+$/'); // specify regex pattern for username, defaults to alphanumeric characters + $usernamePattern = $this->getProperty('usernamePattern','/^\p{L}+$/u'); // specify regex pattern for username, defaults to printable chars only,no punctation or numbers,unicode mode $usernameMinLength = (int)$this->getProperty('usernameMinLength',5); - $usernameMaxLength = (int)$this->getProperty('usernameMaxLength',30); + $usernameMaxLength = (int)$this->getProperty('usernameMaxLength',255); if($usernameMinLength > $usernameMaxLength){ //obviously, $usernameMinLength should be less than $usernameMaxLength, but we'll still check in case of mis-config then we'll swap the 2 values $lesser = $usernameMaxLength; @@ -129,8 +129,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface }else if($key === $passwordColumnName){ $data[$passwordColumnName] = password_hash($password, PASSWORD_DEFAULT); }else{ - $data[$key] = filter_var($value, FILTER_VALIDATE_EMAIL) ? $value : filter_var($value,FILTER_SANITIZE_ENCODED); - //sanitize all other inputs, except for valid or properly formatted email address + $data[$key] = htmlspecialchars($value); } } } @@ -142,11 +141,11 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface * query 2,3 or more times. * As a TEMPORARY WORKAROUND, we'll just attempt to register the new user and wait for the db to throw a DUPLICATE KEY EXCEPTION. */ - }catch(\PDOException error){ + }catch(\PDOException $error){ if($error->getCode() ==="23000"){ return $this->responder->error(ErrorCode::DUPLICATE_KEY_EXCEPTION,'',$error->getMessage()); }else{ - return $this->responder->error(ErrorCode::INPUT_VALIDATION_FAILED,$$error->getMessage()); + return $this->responder->error(ErrorCode::INPUT_VALIDATION_FAILED,$error->getMessage()); } } $users = $this->db->selectAll($table, $columnNames, $condition, $columnOrdering, 0, 1);