By
Dor Shany
January 24, 2023
 |

How to Implement a QR Code Authentication System: Scan to Login

QR codes for "mobile-to-web QR code authentication." This authentication technique uses an already authenticated mobile app to authenticate the web app without the need to follow the same login process. We'll show you how to implement QR code authentication using JavaScript and a few frameworks.

QR codes are digital scannable images that can be used to store and retrieve data. In this article, we'll be using QR codes for "mobile-to-web QR code authentication." This authentication technique uses an already authenticated mobile app to authenticate the web app without the need to follow the same login process. This approach is necessary for faster and more secure authentication.

Some good examples of applications that use mobile-to-web QR code authentication are WhatsApp and WhatsApp Web. In this post, we'll show you how to implement QR code authentication using JavaScript and a few frameworks. Even if you're coming from or using a different programming language, you should be able to follow along with some minor adjustments.

How Mobile-to-Web QR Code Authentication Works

The working principle of mobile-to-web QR code authentication is quite straightforward, but I'll break the process down as much as possible. Without further ado, below is a breakdown of the process QR code authentication follows to verify web apps with an already authenticated mobile app:

1. The Web App Makes Requests for QR Code Generation

When we open the login page on our web app, the page will query the server to generate a QR code. This is the normal client-server communication you know about.

2. The Server Generates QR Code

The server receives the request we sent in step one and gives out a response, which is the generated QR code. Generating a plain QR code is not useful; for us to use our QR code for authentication, we need to encode it with a unique value. In this case, it'll be the name of the channel. To ensure that the maximum level of security is maintained, a channel name will be created using a combination of a unique token and timestamp. We can invalidate the code by setting it to last for some period. Let's say 10 seconds. That means that a new QR code will be generated every 10 seconds.

3. The Web App Receives Response from Server

After making a request, the web app will wait for the server to return a response. When the server sends the response—the generated QR code—the web app will download the QR code and display it on the web page.

4. The Web App Connects to a Messaging Channel for Real-Time Data Transfer

The web app connects to the messaging channel and waits for a message from the mobile app.

5. The Mobile App Scans QR Code Displayed by Web App

The user uses the mobile app to scan the QR code displayed by the web app on the screen. This captures the unique value encoded in the QR code and sends it to the server together with the user's session data. The server on its part can then authenticate the unique value in some way and then send the required user information required by the web app to log in to the user. It is important to remember that this is made possible by the fact that the unique value also serves as a channel name on which the web app is currently listening

6. The Mobile and Web App Exchanges of Top-Level Credentials

The web app, which is connected to a channel, receives the message which contains the login information - for example, an authentication token. The token can be used for the API calls made in the web app. By doing so, our web app user is authenticated.

What Do We Need for QR Code Authentication?

From the above processes, you can infer that we need the following for the implementation of a basic QR code authentication.

An Active Web-Socket Connection

We need an active web-socket connection for the exchange of real-time data like user-tokens, which is essential for our QR code authentication. In this example, we’ll be using a Pusher.

Mobile App

We need a mobile app, which we'll use to log in to the app and capture the QR code displayed by our web app. I’ll use the React Native library for the implementation of the mobile app.

Web App

This is the web app that we want to share our mobile authentication with. I'll be using the Next.js framework in building the web app.

Backend Server

The backend server fulfills one of the most important roles in our QR code authentication, as we use it for the generation of the QR codes we need for authentication. I'll use Express.js for the implementation.

Enough Talk — It's Time to Code Our QR Code Authentication!

We've talked a lot about mobile-to-web QR code authentication. Now it's time to implement it with actual code. Before we proceed, please take note of the following prerequisites:

  • Basic knowledge of Next.js, React Native, Web-socket, and Express.js
  • Basic Knowledge of web and app development
  • Have Nodejs installed

Web App: Part I

This web app will request for QR code, show it, and listen to a messaging channel for the user token. We'll be using a Next.js snippet to illustrate this.



import Head from 'next/head'
import styles from '../styles/Home.module.css'
import {useEffect, useState} from "react";
import Pusher from 'pusher-js';
import axios from "axios"
import apis from "../apis/auth"
import QRCode from "react-qr-code";
import {useCookies} from "react-cookie";
import Router from 'next/router';


// THIS IS LOGIN PAGE


const initPusher = () => {
   Pusher.logToConsole = false;
   return new Pusher(process.env["NEXT_PUBLIC_PUSHER_KEY"], {
       cluster: 'ap2',
       channelAuthorization: {
           endpoint: apis({}).AUTHORIZE_PUSHER.url
       },
   })
}


export default function Login() {
   const [qr_data, setQrData] = useState(null)
   const [isLoading, setLoading] = useState(false)




   const getQRCode = async () => {
       try {
           let res = await axios.get(apis({}).GENERATE_QR.url);
           setQrData(res.data.data)
           return res.data.data
       } catch (e) {
           alert("Cannot fetch QR Data")
       }
       return null
   }


   const handleLogin = async (data)=>{
       const token = data.token
       const user_id = data.user_id
       try {
           let res = await axios.post(apis({}).VERIFY_TOKEN.url, {token, user_id});
           await Router.replace('/profile')
           return res.data.data
       } catch (e) {
           console.error(e)
       }
       return null;
   }


   const showQrCode = ()=>{
       let pusher = initPusher();
       getQRCode().then((res) => {
           setLoading(false)
           if(res){
               const channel = pusher.subscribe('private-' + res.channel);
               console.log("CHANNEL=", res.channel)
               channel.bind('login-event', function (data) {
                   handleLogin(data)
               });
           }
       })
   }


   useEffect(() => {
       setLoading(true)
       showQrCode();
       setInterval(() => {
           showQrCode()
       }, 10000);
   }, [])
   if (isLoading) return <div className={styles.main}>
       <h2>Loading...</h2>
   </div>
   return (
       <div className={styles.container}>
           <main className={styles.main}>
               <div className={styles.title}>
                   Please Scan Login Authenticate
               </div>
               <div style={{marginTop: 50, background: 'white', padding: '16px' }} >
                   {qr_data?<QRCode value={qr_data.channel} size={320}/>:null}
               </div>
           </main>
       </div>


   )
}


Generate QR Code

On the server side, we have to generate tokens on demand from the web app. These tokens are what the web app uses to display a corresponding QR code. A database—or any other form of storage—needs to store the tokens, and preferably they expire after a predefined period.

const generateQR = async (req, res) => {
   const token = crypto.randomBytes(64).toString('hex');
   let channel_data = new Date().getDate() + "-" + new Date().getMonth() + "-" + new Date().getMinutes()
   let channel_data_hash = crypto.createHash('md5').update(channel_data+"||"+token).digest("hex");


   return res.status(200).json({
       success: true,
       msg: "QR DATA Created",
       data: {
           channel: channel_data_hash
       }
   })
}

Mobile App

On our mobile app, we have a logged-in user who can scan the web QR code. The following code shows an extract from a React Native mobile app that handles this process via the handleBarCodeScanned function. Typically, all this function will do is send the token from the QR code and the user data to the server via triggerAuthToken.

import React, { useState, useEffect } from 'react';
import {View, Text, StyleSheet, Button} from 'react-native';
import {connect, useSelector} from "react-redux";
import { Camera } from 'expo-camera';
import * as SecureStore from "expo-secure-store";
import {triggerAuthToken} from "../../apis";




export function App({navigation, ...props}) {
   const [hasPermission, setHasPermission] = useState(null);
   const [scanned, setScanned] = useState(false);
   const user_details = useSelector((state) => state.Auth.user_details)
   useEffect(() => {
       const getBarCodeScannerPermissions = async () => {
           const { status } = await Camera.requestCameraPermissionsAsync();
           setHasPermission(status === 'granted');
       };
       getBarCodeScannerPermissions();


   }, []);


   const handleBarCodeScanned = async ({ type, data }) => {
       setScanned(true);
       alert(`Barcode with type ${type} and data ${data} has been scanned!`);
       const session_token = await SecureStore.getItemAsync("session_token");
       const channel = "private-"+data
       let resp = await triggerAuthToken(session_token, channel, user_details._id)
       console.log(resp)
   };


   const _handleLogout = async ()=>{
       await SecureStore.deleteItemAsync("session_token");
       navigation.reset({
           index: 0,
           routes: [{ name: 'Home' }],
       });
   }


   if (hasPermission === null) {
       return <Text>Requesting for camera permission</Text>;
   }
   if (hasPermission === false) {
       return <Text>No access to camera</Text>;
   }


   return (
       <View style={{flex: 1, alignItems: 'center', justifyContent: 'center'}}>
           <Text>You are logged in</Text>
           <Camera
               onBarCodeScanned={scanned ? undefined : handleBarCodeScanned}
               barCodeScannerSettings={{
                   barCodeTypes: ['qr'],
               }}
               style={{
                   height: '50%',
                   width: '80%',
               }}
           />
           {scanned && <Button title={'Tap to Scan Again'} onPress={() => setScanned(false)} />}
           <View style={{width:170, marginTop:30}}>
               <Button title={"Logout"} onPress={()=>_handleLogout()} color={"red"}/>
           </View>
       </View>
   );
}




function mapStateToProps(state) {
   return {
       auth: state.Auth,
   }
}


const mapDispatchToProps = dispatch => {
   return {}
};
export default connect(mapStateToProps, mapDispatchToProps)(App)

Authenticate User on Server

The next thing we need to be able to accomplish is logging in to the user after they scan the QR code. The server does this by validating auth-tokens sent from the mobile app, creating the user session (authorization token), and sending them to the web app for authentication via WebSocket.

let channel = req.body.channel
let token = req.body.token
let user_id = req.body.user_id
try {
   let resp = await pusher.trigger(channel, "login-event", {
       token,
       user_id
   });
   return res.status(200).json({
       success: true,
       msg: "",
       data: resp
   })
} catch (e) {
   console.log(e)
}
return res.status(200).json({
   success: true,
   msg: "Token Triggered",
   data: {}
})

Web App: Part II

On reception of user information and a valid authorization token, the web app can log in the user. This is what the handleLogin function does.

const channel = pusher.subscribe('private-' + res.channel);
channel.bind('login-event', function (data) {
   handleLogin(data)
});
const handleLogin = async (data)=>{
   const token = data.token
   const user_id = data.user_id
   try {
       let res = await axios.post(apis({}).VERIFY_TOKEN.url, {token, user_id});
       await Router.replace('/profile')
       return res.data.data
   } catch (e) {
       console.error(e)
   }
   return null;
}

If all things are equal, then the user has access to both the mobile app and web app without having to use credentials for the latter.

Conclusion

Overall, QR code authentication is a quick and easy way to log in on another device from an already logged-in client. The working principle is pretty clear and can be useful in many cases. Today, we covered the basic setup of mobile-to-web QR authentication. Try modifying the setup we've covered to meet your needs or experiment with another programming language.

This post was written by Boris Bambo. Boris is a data & machine learning engineer fascinated by technology, education, and business. Feel free to connect with him on LinkedIn.