"an ideal is an idea that you have fallen in love with", Bob Proctor
Log in with QR code is a practical login approach to the classic username and password. How does this method work exactly and is it as secure as we think it is? On this article we explore the technology behind QR code login which seems to be taking over many technologically advanced sites such as cryptocurrecy exchanges. Please note that I will not be presenting the specific approach used by any one platform but the QR login concept general.
OK, so to begin, let's first discuss the QR login process.
A user follows the below steps to login on the browser:
1. Visits the platform website that provides the login screen with the QR code login option.
2. The user opens the relevant app on their phone that has the capability to provide the 'scan to login' option
3. User scans the QR code displayed
4. The user clicks authorize on the phone app to allow the new device, in this instance the browser they are trying to login to
5. The web client page refreshes to the deisgnated 'Home' page which is usually displayed following a successful login
As a rule of thumb, for a browser to be in a logged in state, a session id is stored, as a cookie, in a browser. Every time a request is made to the server by the client, the browser/app is instructed to send the session id together with that request.
So essentially, once a user scans the QR code on a browser, they either share their session id from their logged in app, or they instruct the server to create a new authenticated session id and assign it to the device that initially displayed the QR code.
The technology used behind the user interface on the browsers' side is 'websockets'. This is a form of a TCP connection with a server which typically exchanges messages in the form of JSON strings.
In this example a client machine sends a request for a QR code image to a server which then sends the QR code back to the client. The image contains a unique identifier for that client machine via the browser. The identifier is also stored on a server which awaits for a message to link that with a session id from an authenticated mobile device.
The mobile device scans the QR code, in the above screenshot example the client id is stored as part of a URL (https://www.binance.com/en/qr/f6a71b7736d74ba59fca478f77d7ef9c). The authenticated device then sends their session id to the server and requests that the client with the above id is granted either the same or a new session id.
The technical aspect.
For our simple example we will use:
Webserver to host the Angularjs client (index.html):
<!DOCTYPE html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.9/angular.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/gdi2290/angular-websocket@v1.0.9/angular-websocket.min.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.9/angular-sanitize.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
</head>
<body>
<div ng-app="MyAwesomeApp" ng-controller="MyCtrl">
<h1><p ng-bind-html="msg"></p> {{serveraddr}}:{{serverport}}</h1>
<div ng-show="!authenticated">
<img style="width: 250px; height: 250px;" ng-src="data:image/png;base64,{{imgsrc}}"/>
</div>
<hr />
<div ng-show="authenticated">
Authenticated with phone scan app.
</div>
</div>
</body>
<script src="/app.js"></script>
</html>
Webserver to host the Angularjs client (app.js):
'use strict';
var app = angular.module('MyAwesomeApp', ['angular-websocket', 'ngSanitize']);
app.controller('MyCtrl', function ($scope, $websocket, $http) {
let uid = Math.floor(Math.random() * 100000) +1000;
var vm = this;
$http.get("LoginLess_QR.php?uid="+uid)
.then(function(response) {
console.log(response);
let uid_data = response.data;
$scope.imgsrc = uid_data.imgsrc;
vm.openConnection();
});
$scope.ws = $websocket();
$scope.serveraddr = 'ws://192.168.2.27';
$scope.serverport = '8082';
$scope.messageInputBox = '{"client_id":"'+uid+'"}';
$scope.msg = "click to connect...";
$scope.authenticated = false;
vm.closeConnection = function() {
console.log('Closing from client connection with state ' + $scope.ws.readyState);
//$scope.data = '{"close_connection": "true"}';
$scope.data = 'bye';
if($scope.ws.readyState != $websocket.CLOSED) {
$scope.ws.send($scope.data);
$scope.ws.close();
}
};
vm.openConnection = function() {
$scope.msg = '<i class="fa fa-refresh fa-spin" style="font-size:24px"></i>';
console.log('Opening connection...');
$scope.ws = $websocket($scope.serveraddr+':'+$scope.serverport)
.onOpen(function(message){
vm.logMessage('connected to...');
$scope.msg = 'Connected';
console.log('connected...' + JSON.stringify(message));
vm.sendMessage();
})
.onClose(function(message){
$scope.msg = 'connection closed with...';
console.log('connection closed...' + JSON.stringify(message));
})
.onMessage(function(message){
console.log(message);
let ws_incoming = JSON.parse(message.data);
if(ws_incoming.authkey_data.includes("AUTH_")){
$scope.authenticated = true;
}
//let parsed_data = JSON.parse(incomingData.uid);
let authkey_data = JSON.parse(ws_incoming.authkey_data);
//$scope.msg = "GOT KEY: " + authkey_data.authkey; //optionally show the received key
$scope.msg = '<i class="fa fa-check" style="color: green; font-size:24px"></i>';
console.log("GOT KEY: " + authkey_data.authkey);
});
};
vm.sendMessage = function() {
console.log('Sending message');
$scope.data = $scope.messageInputBox;
$scope.ws.send($scope.data);
};
vm.logMessage = function(aMsg){
$scope.msg = aMsg;
};
});
PHP Server script to provide the QR code as an base 64 encoded image(LoginLess_QR.php):
<?php
date_default_timezone_set("Europe/Nicosia");
if (isset($_GET['uid'])) {
include "qrlib.php";
$QRC_LOCAL_URL = "";
$errorCorrectionLevel = 'L';
$matrixPointSize = 4;
define('IMAGE_WIDTH',150);
define('IMAGE_HEIGHT',150);
$trnData['uid'] = intval($_GET['uid']);
$imageData = $QRC_LOCAL_URL.$trnData['uid'];
ob_start();
QRCode::png($imageData, null);
$imageString = base64_encode( ob_get_contents() );
ob_end_clean();
echo json_encode(array('uid'=>$imageData, 'imgsrc'=>$imageString));
}
else
{
echo 'NaN';
}
?>
PHP Server script to save the mapping between an authenticated client and a new client as a new file (LoginLess_Mapping.php):
Websocket Server that exchanges messages between a client machine and checks for any authenticated ids to provide (Note that this server was written for an ESP8266):
/**
Websocket Server and HTTP Client.ino
Created on: 26.04.2022
*/
#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <ESP8266WiFiMulti.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClient.h>
#include <ArduinoWebsockets.h>
#include <Arduino_JSON.h>
ESP8266WiFiMulti WiFiMulti;
using namespace websockets;
WebsocketsServer server;
void setup() {
Serial.begin(115200);
// Serial.setDebugOutput(true);
Serial.println();
Serial.println();
Serial.println();
for (uint8_t t = 4; t > 0; t--) {
Serial.printf("[SETUP] WAIT %d...\n", t);
Serial.flush();
delay(1000);
}
WiFi.mode(WIFI_STA);
WiFiMulti.addAP("SSID", "PASSWORD");
Serial.print("Connecting to WiFi ..");
while (WiFiMulti.run() != WL_CONNECTED) {
Serial.print('.');
delay(1000);
}
Serial.println("IP address: ");
Serial.println(WiFi.localIP()); //You can get IP address assigned to ESP
server.listen(8082);
Serial.print("Is server live? ");
Serial.println(server.available());
}
void loop() {
WebsocketsClient clientsv = server.accept();
while(clientsv.available()) {
WebsocketsMessage msg = clientsv.readBlocking();
// log
Serial.print("Got Message: ");
Serial.println(msg.data());
// blinkLight(2);
String received_data = msg.data();
JSONVar incomingRequest = JSON.parse(received_data);
if (incomingRequest.hasOwnProperty("client_id")) {
String uid = "";
int countTries = 0;
uid = retrieveHTTPData((const char*) incomingRequest["client_id"]);
//keep checking up to 12 seconds, modify according to your needs or just remove the second part of the if statement
while(uid == "" && countTries < 12){
//wait 1 second and try again
delay(1000);
Serial.println("Attempting to retrieve key " + countTries);
uid = retrieveHTTPData((const char*) incomingRequest["client_id"]);
countTries++;
}
if(uid != "") {
JSONVar outgoingRequest;
outgoingRequest["client_id"] = (const char*) incomingRequest["client_id"];
outgoingRequest["authkey_data"] = uid;
Serial.print("outgoingRequest.keys() = ");
Serial.println(outgoingRequest.keys());
// Send the message to the client with the found id key
String jsonString = JSON.stringify(outgoingRequest);
clientsv.send(jsonString);
delay(100);
Serial.println("Closing connection with client");
// close the connection
clientsv.close();
}
}
if(incomingRequest.hasOwnProperty("close_connection")){
Serial.println("Closing connection with client");
// close the connection
clientsv.close();
//to do delete mapping from db by sending relevant request
}
}
delay(50);
}
WiFiClient client;
HTTPClient http;
Serial.print("[HTTP] begin...\n");
if (http.begin(client,
//"http://serveraddress/LoginLess_Mapping.php?uid=" + uid)
"http://serveraddress/" + uid + ".txt")
){ // HTTP
Serial.println("Getting key from http://serveraddress/" + uid + ".txt");
Serial.print("[HTTP] GET...\n");
// start connection and send HTTP header
int httpCode = http.GET();
// httpCode will be negative on error
if (httpCode > 0) {
// HTTP header has been send and Server response header has been handled
Serial.printf("[HTTP] GET... code: %d\n", httpCode);
// file found at server
if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY) {
String payload = http.getString();
Serial.println(payload);
return payload;
}
} else {
Serial.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str());
}
http.end();
return "";
} else {
Serial.printf("[HTTP} Unable to connect\n");
return "";
}
}