Authentication: Register & Login with JWT in PHP & MYSQL

In the previous tutorial, you learnt to upload form data containing text fields and and image. This tutorial teaches you how to do authentication in PHP & MYSQL using JWT and restrict APIs to allow only authenticated users. From a client app, a visitor can register  a new user account using username, email, and password. The password is encrypted using PHP password_hash function before saving in database. The registered user logs in to the system using username and password. Behind the scene, after a successful login, a valid token will be generated on the server and sent to the client. Then whenever the client accesses the restricted APIs, it must send the token to be verified on the server.
To generate and verify token in PHP, in the api folder, run the following command to install php-jwt:

api>composer require firebase/php-jwt

Execute the following command to create a migration file to create jwtusers.
./vendor/bin/phinx create JwtUsersTableMigration

<?php
declare(strict_types=1);

use Phinx\Migration\AbstractMigration;

final class JwtUsersTableMigration extends AbstractMigration
{
   
    public function change(): void
    {
        
        $table = $this->table('jwtusers');
        $table->addColumn('username', 'string', ['limit' => 30])
        ->addColumn('fullname', 'string', ['limit' => 50])
        ->addColumn('email_address', 'string', ['limit' => 100])
        ->addColumn('isAdmin', 'integer',['default' => 0])
        ->addColumn('password', 'string', ['limit' => 200])
        ->addColumn('created', 'timestamp', array('default' => 'CURRENT_TIMESTAMP'))
        ->addColumn('token', 'string');

        $table ->create();
       

    }
}

Then run /vendor/bin/phinx migrate to create jwtusers table in the database.

In the Controllers folder, create AutheController.java.

<?php 

namespace App\Controllers;
use App\DB\Database;
use Symfony\Component\Routing\RouteCollection;
use \Firebase\JWT\JWT;
class AuthController
{
	private $con;

	function __construct()
	{
		
		$db = new Database();
		$this->con = $db->connect();
		
			
	}
   
	public function registerAction(RouteCollection $routes)
	{
	
		
		// Get the posted json data.
		$postdata = file_get_contents("php://input"); 
		

        if(isset($postdata) && !empty($postdata))
        {
			// Extract the data.
			$request = json_decode($postdata);
			  // Validate.
			if(trim($request->username) === '' || $request->password ==='')
			  {
				echo json_encode(array("data" => "Empty username or password!"));
			  }
			
			$user_name = $request->username;
			$email_address = $request->email;
$password = $request->password;
                        // create encrypted password    
			$password = password_hash($password, PASSWORD_BCRYPT);

			$isAdmin=0;
	

			$query = "INSERT INTO `jwtusers`(`fullname`, `username`, `email_address`, `password`,`isAdmin`) values(?,?,?,?,?)";

			if($stmt = $this->con->prepare($query)){
				$stmt->bind_param("sssi",$user_name,$email_address, $password,$isAdmin);
				$stmt->execute();
				echo json_encode(array("data" => $request));
			}
			else{
				echo json_encode(array("data" => $this->con->error));
			}
      }
	  
	}
	public function loginAction(RouteCollection $routes)
	{
		

		$api_data = json_decode(file_get_contents("php://input"));
		$user_name = $api_data->username;
		$password1 = $api_data->password;
		$numOfRows=0;
		$query= "SELECT `id`, `fullname`, `username`, `email_address`, `password`, `isAdmin` FROM `jwtusers` WHERE username =?  LIMIT 0,1";

		if($stmt = $this->con->prepare($query)){
		$stmt->bind_param('s', $user_name);
		$stmt->execute();
		$stmt->store_result();
		$numOfRows = $stmt->num_rows;
		
		$stmt->bind_result($user_id, $fullname,$username,$email_address,$password,$isAdmin);
		}
		else{
			echo $this->con->error;
		}
		
		if($numOfRows > 0){
			
			 while ($stmt->fetch()) {
             
		
				$user_id    = $user_id;
				$fname = $fullname;
				$uname = $username;
				$email      = $email_address;
				$pass     = $password;
				
			if(password_verify($password1, $pass))
			{
				$secret_key = "darasecretttt";
				$issuer_claim = "localhost"; 
				$audience_claim = "THE_AUDIENCE";
				$issuedat_claim = time(); // time issued 
				$expire_claim = $issuedat_claim + 6000; // expired time

				$token = array(
					"exp" => $expire_claim,
					"data" => array(
						"user_id" => $user_id,
						"fullname" => $fname,
						"username" => $uname,
						"email_address" => $email_address
				));
                                // generate token
				$jwtValue = JWT::encode($token, $secret_key,'HS256' );
				// return payload
				echo json_encode(					
					array(
						"user_id" => $user_id,
						"username" => $uname,
						"email_address" => $email_address,
						"isAdmin" => $isAdmin,
						"token" => $jwtValue,
						)
					);
			}
			else{
					echo json_encode(array("success" => false,"data" => ['pwd' =>$password1,'pwddb' =>$pass]));
				}
		      }	
			}
			else{
				echo json_encode(array("success" => false));
			}
			
	}
	
}

Then add register and login routes to routes/web.php.
$routes->add('register', new Route(constant('URL_SUBFOLDER') . '/register', array('controller' => 'AuthController', 'method'=>'registerAction'), array()));
$routes->add('login', new Route(constant('URL_SUBFOLDER') . '/login', array('controller' => 'AuthController', 'method'=>'loginAction'), array()));

Now you can use ARC chrome app to register a new user by accessing the http://localhost/mysite/api/register and login via http://localhost/mysite/api/login urls.

Product APIs

In the app folder, create Utils folder and place JwtAuth.php file. JwtAuth is a helper class to get token from request and verify the token.

Utils/JwtAuth.php
<?php

namespace App\Utils;
use \Firebase\JWT\JWT;
use \Firebase\JWT\ExpiredException;
use \Firebase\JWT\SignatureInvalidException;
use Firebase\JWT\Key;

class JwtAuth
{
  private function  getSecret(){
    return ("darasecretttt");
  }
  public function getToken(){
    $token=null;
    if (isset($_SERVER['Authorization'])) {
      $token = trim($_SERVER["Authorization"]);
    }
    else if (isset($_SERVER['HTTP_AUTHORIZATION'])) { //Nginx or fast CGI
      $token = trim($_SERVER["HTTP_AUTHORIZATION"]);
    }
    elseif (function_exists('apache_request_headers')) {
      $headerStringValue = apache_request_headers();  
      $token=@$headerStringValue['Authorization'];
    }
    return $token;
  }
  public function verifyToken($token){
      if($token !=null){
        try{
         
          JWT::decode($token , new Key($this->getSecret(), 'HS256'));
          return (['status'=>200,'data' => 'valid token']);

        }catch (ExpiredException $e) {
          return (['status'=>401,'data' => 'token expired']);
        }
        catch (SignatureInvalidException $e) {
          return (['status'=>401,'data' => 'invalid token signature']);
        }
        catch (Exception $e) {
          return (['status'=>401,'data' => 'invalid token']);
        }

      }
      else return (['status'=>401,'data' => 'unauthorized']);

  }

}


Now we restrict the products APIs to allow only authorized users.  Update Controllers/ProductController.php to use JwtAuth.
...................
use App\Utils\JwtAuth;
class ProductController
{
    private $con;
    private $jwtAuth;
    function __construct()
    {
       
        $db = new Database();
        $this->con = $db->connect();
        $this->jwtAuth=new JwtAuth();

           
    }
.................

Add the following code to a route method that need to be protected:
    // verify token
    $token = $this->jwtAuth->getToken();
        $res = $this->jwtAuth->verifyToken($token);
        if($res['status'] !==200) { // token verification failed
            echo json_encode($res);
            exit();
        }

For example, to protect the route to get products from the database, add the code block above the to getProducts() function as below:
 public function getProducts(int $start,int $limit, string $searchText)
    {
      // verify token
        $token = $this->jwtAuth->getToken();
        $res = $this->jwtAuth->verifyToken($token);
        if($res['status'] !==200) { // token verification failed
            echo json_encode($res);
            exit();
        }
        // valid token so fetch products data from database
        $products = [];
        $sql = "SELECT * FROM products WHERE name LIKE '%$searchText%'";
        if($start!==''){
            $sql .= " LIMIT $start,$limit";
        }
       
        if($result = mysqli_query($this->con,$sql))
        {
          $cr = 0;
          while($row = mysqli_fetch_assoc($result))
          {
            $products[$cr]['id']    = $row['id'];
            $products[$cr]['name'] = $row['name'];
            $products[$cr]['description'] = $row['description'];
            $products[$cr]['price'] = $row['price'];
            $products[$cr]['slug'] = $row['slug'];
            $products[$cr]['thumbnail'] = $row['thumbnail'];
            $products[$cr]['category'] = $row['category_id'];
            $products[$cr]['subcategory'] = $row['subcategory_id'];
            $cr++;
          }
          echo json_encode(['data'=>$products,'status' => 200]);
         
        }
        else
        {
          http_response_code(404);
        }
       
       
    }

If you try to access http://localhost/mystie/api/products without a valid token, you will get the "unauthorized" message.


Comments

Popular posts from this blog

APIs to Upload form data with file in PHP & MYSQL

Create Angular App & SideBar

PHP Mysql Database Migration Using Phinx