DEV Community

Cover image for QR login like whatsapp using webocket php
Sahil kashyap
Sahil kashyap

Posted on • Edited on

QR login like whatsapp using webocket php

QR login using websocket in laravel

  • QR test page in incognito mode
    QR test page in incognito mode

  • When QR scanned
    When QR scanned successfully

  • Things happening in the websocket
    WS console

package used:

  1. cboden/ratchet

Our app and websocket are on different port.
Let's setup command to run the websocket

<?php
namespace App\Console\Commands;

use Illuminate\Console\Command;

use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
use App\Http\Controllers\WebSocketController;
use React\EventLoop\Factory;
use React\Socket\SecureServer;
use React\Socket\Server;
class WebSocketServer extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'websocket:init';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Initializing Websocket server to receive and manage connections';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    { //for local
        // $this->forlocal();


        //for prod server
        $this->forprodserver();

    }

    public function forlocal()
    {
        $server = IoServer::factory(new HttpServer(new WsServer(new WebSocketController())) , 8090);
        $server->run();
    }

    public function forprodserver()
    {
        $loop = Factory::create();
        $webSock = new SecureServer(new Server('0.0.0.0:8090', $loop) , $loop, array(
            'local_cert' => '/etc/letsencrypt/live/test.tv.com/fullchain.pem', // path to your cert
            'local_pk' => '/etc/letsencrypt/live/test.tv.com/privkey.pem', // path to your server private key
            'allow_self_signed' => true, // Allow self signed certs (should be false in production)
            'verify_peer' => false
        ));
        // Ratchet magic
        $webServer = new IoServer(new HttpServer(new WsServer(new WebSocketController())) , $webSock);
        $loop->run();
    }
}

Enter fullscreen mode Exit fullscreen mode

Let's setup the routes
web.php

<?php
Route::get('/qrtesting', 'Admin\QRLoginTwoController@qrtesting');
Route::post('web/loginws', 'Admin\QRLoginTwoController@loginWS');
Route::get('/qrscanner', 'Admin\QRLoginTwoController@qrscanner2');
Enter fullscreen mode Exit fullscreen mode

Controller

<?php
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class QRLoginTwoController extends Controller
{
    public function qrtesting()
    {

        return view('frontend.qrtesting');

    }
    public function qrscanner2()
    {
        if (Auth::check())
        {
            $login = true;

            return view('frontend.qrscanner2', compact('login'));
        }
        return redirect()->route('home');
    }
    public function loginWS(Request $request)
    {
        $key = $request['key'];
        if (empty($key))
        {

            $return = array(
                'status' => 2,
                'msg' => 'key not provided'
            );
            return response()->json($return, 200);
        }

        $userid = UnHashUserID($key);
        try
        {
            $user = Auth::loginUsingId($userid, true);
            $return = array(
                'status' => 1,
                'msg' => 'success',
                'jwt' => 1,
                'user' => $user
            );
            return response()->json($return, 200);
        }
        catch(Exception $exception)
        {

            return response()->json(['status' => 2, 'success' => false, 'message' => 'Some Error occured', 'error' => $exception->getMessage() , 'response_code' => 200,

            ], 200);
        }

    }

}
?>
Enter fullscreen mode Exit fullscreen mode

WebSocketController.php

<?php
namespace App\Http\Controllers;

use Illuminate\Support\Str;
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;

class WebSocketController extends Controller implements MessageComponentInterface{
    private $connections = [];

    private $clients;
    private $cache;

    public function __construct()
    {
        $this->clients = new \SplObjectStorage();
        // memory cache
       $this->cache = array();
    }
    public function multicast($msg) {
        foreach ($this->clients as $client) $client->send($msg);
    }

    public function send_to($to,$msg) {
        if (array_key_exists($to, $this->clientids)) $this->clientids[$to]->send($msg);
    }
     /**
     * When a new connection is opened it will be passed to this method
     * @param  ConnectionInterface $conn The socket/connection that just connected to your application
     * @throws \Exception
     */
    function onOpen(ConnectionInterface $conn){
        $this->clients->attach($conn);

        echo "New connection! ({$conn->resourceId})\n";
  }

     /**
     * This is called before or after a socket is closed (depends on how it's closed).  SendMessage to $conn will not result in an error if it has already been closed.
     * @param  ConnectionInterface $conn The socket/connection that is closing/closed
     * @throws \Exception
     */
    function onClose(ConnectionInterface $conn){
        unset($this->cache[$conn->resourceId]);
        $this->clients->detach($conn);
        echo "Connection {$conn->resourceId} has disconnected\n";
        $this->clients->detach($conn);
    }

     /**
     * If there is an error with one of the sockets, or somewhere in the application where an Exception is thrown,
     * the Exception is sent back down the stack, handled by the Server and bubbled back up the application through this method
     * @param  ConnectionInterface $conn
     * @param  \Exception $e
     * @throws \Exception
     */
    function onError(ConnectionInterface $conn, \Exception $e){
        echo "An error has occurred: {$e->getMessage()}\n";
        $conn->close();
    }

     /**
     * Triggered when a client sends data through the socket
     * @param  \Ratchet\ConnectionInterface $conn The socket/connection that sent the message to your application
     * @param  string $msg The message received
     * @throws \Exception
     */
    function onMessage(ConnectionInterface $from, $msg){
        $numRecv = count($this->clients) - 1;
        echo sprintf('Connection %d sending message "%s" to %d other connection%s' . "\n"
            , $from->resourceId, $msg, $numRecv, $numRecv == 1 ? '' : 's');
            $obj = json_decode($msg);

$type = $obj->type;
if($type=='client'){
    switch ($obj->step) {
        case 0:
            // echo "\n inside client,step0 \n";
            $token = $obj->token;
            $theuuid = UnHashUserID($token);

            //todo add jwt with 2minutes of token
            $tokenexist=array_key_exists($theuuid, $this->cache);

            if($tokenexist){
                echo "\n token exist ya \n";
                $ee=$this->cache[$theuuid]; 
                // print_r($ee);
                if($ee['status']=='0'){
                    $this->cache[$theuuid]['status'] = 1;
                    $this->cache[$theuuid] += ['child' => $from];
                    $myArray2[] = (object) ['step' => 1];
                    $Scan = new \SplObjectStorage();
            $Scan->code=0;
            $Scan->data=$myArray2[0];
            $Scan->msg="Scan code successfully";
            $this->cache[$theuuid]['parent']->send(json_encode($Scan));

            $ready2 = new \SplObjectStorage();
            $ready2->code=0;
            $ready2->data=$myArray2[0];
            $ready2->msg="Ready";
            $from->send(json_encode($ready2));
                };
            }else{

                echo "token doesn't exsit";
            }
            break;
            case 1:
                $myArray3[] = (object) ['step' => 2];
                $myArray4[] = (object) ['step' => 2,'username'=>$obj->username];
                foreach ($this->cache as $v) {
                    if($v['child']==$from){
                        // $token updateSessionToken;

            $ready3 = new \SplObjectStorage();
            $ready3->code=0;
            $ready3->data=$myArray4[0];
            $ready3->msg="Already logged in";
            if(array_key_exists("parent", $v)){}
                    $v['parent']->send(json_encode($ready3));
                    }
                }

            $ready = new \SplObjectStorage();
            $ready->code=0;
            $ready->data=$myArray3[0];
            $ready->msg="Login successful";
            $from->send(json_encode($ready));
    }
}else if($type=='server'){
    // echo "hello inside server";
    //to get the QR logo
    switch ($obj->step) {
        case 0:
            $uuid = $from->resourceId;//Str::random(30);
             echo $uuid;
            $token = HashUserID($uuid);
            // echo $token;
            $this->cache[$uuid] = [ 'status'=> 0, 'parent'=> $from ];
            $url = url(''); // Get the current url 
            // dd($url);
           $http = $url .'?t='.$token; // Verify the url method of scanning code 

            $myArray[] = (object) ['step' => 0,'url' => $http];
            $ready = new \SplObjectStorage();
            $ready->code=0;
            $ready->data=$myArray[0];
            $ready->msg="Ready";
            $from->send(json_encode($ready));

            break;


    }
}
    }

}
Enter fullscreen mode Exit fullscreen mode

Let's generate the QR code:qrtesting.blade.php

<!DOCTYPE HTML>
<html>

<head>


    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
    <script src="../frontend/qr/jquery.qrcode-0.11.0.min.js" ></script>
    <script type="text/javascript">
        $(document).ready(function() {
            initiate();

        });

        function initiate() {
            if ("WebSocket" in window) {
                var base = window.location.hostname;
               // var ws = new WebSocket('wss://'+base+':8090');
                var ws = new WebSocket('wss://'+base+':8090');
                console.log(ws);
                ws.onopen = function() {
                   ws.send(JSON.stringify({ type: "server", code: 0, step: 0 }));

                };
                ws.onmessage = function(evt) {
                   const data = JSON.parse(event.data);
                   //console.log("datafromservver",data);
                   const step = data.data && data.data.step;
                           if (step === 0) {
 //Generate QR Code and show to user.
                        $("#qrcode").qrcode({
                            "width": 100,
                            "height": 100,
                            "text": data.data.url
                        });
                          console.log("QR code generated successfully");

        }  else if (step === 2) {
            const { username, token } = data.data;
            //localStorage.setItem(TOKEN_KEY, token);

           $("#qrcode").html("");
            ws.close();
//alert(username);
is_loginfun(username);
        }            



                };


                ws.onclose = function() {
                    console.log("Connection is closed...");
                };
            } else {
                alert("WebSocket NOT supported by your Browser!");
            }
        }


// Check whether the login has been confirmed 
function is_loginfun(param){
      var key = param;
      console.log("is_login called");
    $.ajax({
        type: "POST" ,
        dataType: "json" ,
        url: "web/loginws" ,
        data:{
            key:key ,
              "_token":"<?php echo  csrf_token() ?>"
        },

            headers: {'x-csrf-token': '<?php echo  csrf_token() ?>'}, 
        success:function(data) {
              if (data.status==1 ){
                  var uid = data.jwt;
                  var user = data.user;
                  console.log("user",user);

                console.log("login successfull",uid);

                alert("login successfull",uid);
                  window.location.href = '/';

            } else  if (data.status==2 ){
                alert(data.msg);
            }
        }
    });
}
    </script>

    <body>
        <br>
        <br>
        <div align="center">
            <div id="qrcode">
              <img src='iconLoading.gif' />
            </div>
            <div id="profile"></div>
        </div>

    </body>

</html>
Enter fullscreen mode Exit fullscreen mode

QRscanner:qrscanner2.blade.php
We scan and get the data from qr code and send the data

<!DOCTYPE HTML>
<html>

<head>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
    <script src="../frontend/qr/jquery.qrcode-0.11.0.min.js" ></script>

</head>
    <section class="page-section">
        <div class="container">
                <h2 class="section-heading text-center">QR code scanner</h2>
                <div class="row setting-cards">
                    <div class="col-centered col-md-8">
                        <ul class="setting-card"> 
                        <li class="text-center">

                 <?php $hashedid= HashUserID(Auth::user()->id); ?>
                  <p>passcode: <?php echo $hashedid; ?></p>
                  <p>Name: <?php echo Auth::user()->name;?></p>
                  <p>Email: <?php echo Auth::user()->email;?></p>
                   </li>
                        <li class="text-center">
    <div id="qr-reader" class="col-md-8"></div>

     <p id="login_mobile_scan_qrcode"></p>
     <p id="qrcodedoLogin"></p></li>
                        </ul>
                         <div id="qr-reader-results"></div> 
                    </div>
                </div>




        </div>

    </section><section class="page-section">


    </section>




</body>

<script src="../frontend/qr/html5-qrcode.min.js" ></script>
<script>
function qrcodedoLogin(param){
      var url = param;
      console.log("qrcodedoLogin called",url);
    $.ajax({
        type: "POST" ,
        dataType: "json" ,
        url: url ,
        data:{
            //key:key
        },
        success:function(data) {
              if (data.status==1 ){
                  var qrcodeloginurl = data.msg;
            //scan successfull url recieved

                  $('#qrcodedoLogin').text("QR Loggin successfully");
               // console.log("qrcodeloginurl",qrcodeloginurl);
//qrcodedoLogin(qrcodeloginurl);

            } else  if (data.status==2 ){
//couldn't do login
               // alert(data.msg);

                  $('#qrcodedoLogin').text(data.msg);
            }
        }
    });
}
function login_mobile_scan_qrcode(param){
      var url = param;
      if ("WebSocket" in window) {
          var base = window.location.hostname;
        var ws = new WebSocket('wss://'+base+':8090');
                ws.onopen = function() {
            console.log("on WS open we sent the token to server");
            let params = (new URL(url)).searchParams;
            let urltoken = params.get('t'); 
            ws.send(JSON.stringify({ type: "client", step: 0, token: urltoken }));

                };
                ws.onmessage = function(event) {
                    const data = JSON.parse(event.data);
                    console.log(" client body",data);
                     const step = data.data && data.data.step;
                           if (step === 0) {
console.log("step",step);
                           }else if (step === 1) {
 ws.send(JSON.stringify({ type: "client", step: 1, username:'<?php echo $hashedid?>' }));
                           }

                }
                ws.onclose = function() {
                    console.log("Connection is closed...");
                };
      } else {
                alert("WebSocket NOT supported by your Browser!");
            }
     // console.log("login_mobile_scan_qrcode called",url);

}
    function docReady(fn) {
        // see if DOM is already available
        if (document.readyState === "complete"
            || document.readyState === "interactive") {
            // call on next available tick
            setTimeout(fn, 1);
        } else {
            document.addEventListener("DOMContentLoaded", fn);
        }
    }

    docReady(function () {
        var resultContainer = document.getElementById('qr-reader-results');
        var lastResult, countResults = 0;
        function onScanSuccess(decodedText, decodedResult) {
            if (decodedText !== lastResult) {
                ++countResults;
                lastResult = decodedText;
                // Handle on success condition with the decoded message.
                console.log(`Scan result ${decodedText}`, decodedResult);
                 resultContainer.innerHTML += `<div>[${countResults}] - ${decodedText}</div>`;


                login_mobile_scan_qrcode(decodedText);
                 // Optional: To close the QR code scannign after the result is found
           // html5QrcodeScanner.clear();
            }
        }

        var html5QrcodeScanner = new Html5QrcodeScanner(
            "qr-reader", { fps: 10, qrbox: 250 });
        html5QrcodeScanner.render(onScanSuccess);
    });
</script>
Enter fullscreen mode Exit fullscreen mode

Gist
Github of the projct

Top comments (6)

Collapse
 
naeemijaz profile image
Naeem Ijaz

Sir it is working fine on local server
but when i upload it to the remote server it is giving me error

WebSocket {url: 'wss://qberg.mn/:8080', readyState: 0, bufferedAmount: 0, onopen: null, onerror: null, …}
qrtest:48 WebSocket connection to 'wss://qberg.mn/:8080' failed: Error during WebSocket handshake: Unexpected response code: 404
(anonymous) @ qrtest:48
dispatch @ jquery.min.js:2
v.handle @ jquery.min.js:2
qrtest:90 Connection is closed...

Collapse
 
sahilkashyap64 profile image
Sahil kashyap

hi Naeem, Apologies for late reply. have you setup-ed the the wss properly on server.

  • The error message you shared says 404, maybe the url is wrong.
    • if it is not, Maybe try pinging the url using postman
Collapse
 
gimple profile image
gimple

very cool!

Collapse
 
sahilkashyap64 profile image
Sahil kashyap

thank you,I've added the github link, github.com/sahilkashyap64/qrlogin-...

Collapse
 
alexvranich1 profile image
Alex vranich

How do I get the username that scanned the qr to be saved in a "Starts" table along with the time that person scanned the qr code

Collapse
 
sahilkashyap64 profile image
Sahil kashyap

Get username who scanned the qr code
And make entry in Starts table?
If i'm understanding it wrong, kindly correct me.

here's the solution:

in this function

public function loginWS(Request $request)
Enter fullscreen mode Exit fullscreen mode

I look for a

$key

//$key is just hashed userID,i unhash it
Enter fullscreen mode Exit fullscreen mode

And then I login

$user = Auth::loginUsingId($userid, true);
// $user contains all the user details
Enter fullscreen mode Exit fullscreen mode

/**
2 Ways to make entry in Starts table

  1. laravel provides ways to overRide login function,
  2. Simply add Starts Model in top and make an entry like this
use App\Models\Starts;
//place this query right after $user = Auth::loginUsingId($userid, true);
$Starts = Starts::create([
        'userId' => $userid,
          'somerandominfofromuser' => $user->dummycolum,
        //asumming createdat or updatedat fields are autofilled and the model is properly setuped
    ]);
Enter fullscreen mode Exit fullscreen mode

*/

Unrelated to above here's a nice doc of laravel not so popular tips