diff --git a/README.md b/README.md index ce119d3c..260307ec 100644 --- a/README.md +++ b/README.md @@ -711,7 +711,11 @@ 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) +- "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 ("") @@ -939,10 +943,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` Also configure the `jwtAuth.audiences` (fill in the Firebase project ID). @@ -1006,7 +1010,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'); }, diff --git a/src/Tqdev/PhpCrudApi/Middleware/DbAuthMiddleware.php b/src/Tqdev/PhpCrudApi/Middleware/DbAuthMiddleware.php index fb6aba9f..8d9dad3b 100644 --- a/src/Tqdev/PhpCrudApi/Middleware/DbAuthMiddleware.php +++ b/src/Tqdev/PhpCrudApi/Middleware/DbAuthMiddleware.php @@ -71,6 +71,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','/^\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',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; + $usernameMaxLength = $usernameMinLength; + $usernameMinLength = $lesser; + } $passwordLength = $this->getProperty('passwordLength', '12'); $pkName = $table->getPk()->getName(); $registerUser = $this->getProperty('registerUser', ''); @@ -95,15 +104,50 @@ 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); - $this->db->createSingle($table, $data); + $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] = htmlspecialchars($value); + } + } + } + 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) { if ($loginAfterRegistration) { @@ -111,6 +155,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 { @@ -128,6 +173,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); } @@ -176,6 +222,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, '');