Sayantam Dey on Product Development

Password-less Web Login with a Mobile App

Apr 17, 2022

If you have used Whatsapp web, you have experienced the use case explored in this post. This post is a proof-of-concept to demonstrate a web login without a password by using a mobile app. The user's flow is as follows:

  • A user logs into the mobile app.
  • They open the web version in a browser and see a QR code.
  • They use the mobile app to scan the QR code, and after a couple of seconds, the browser refreshes to show an authenticated page.

System Context

System Context

High-Level Flow

A user logs into the mobile application, which receives an authentication token.

  1. The user navigates to the web app login page. First, the login page displays the QR code. Next, it waits for a signal to navigate to an authenticated page. The QR code encodes a session identifier also stored in a response cookie.
  2. The user uses the app to scan the QR code, which sends a POST request with the auth token and decoded session identifier to a login API. The app may also send other helpful information like the username.
  3. Upon successfully validating the session identifier and user information, the login endpoint associates the user information with the session identifier.
  4. The web page navigates to an authenticated page after a timeout or on getting a signal, sending back the session cookie it received from step #2. Since the user information is already associated with the session identifier from step #4, the user is identified and shown the authenticated page.

Web App

The web app shows the QR code by requesting the API for one. The snippet shows how:

<img src="http://localhost:3000/qr-code" width="150" height="150" /> 

After the page is displayed, the web app needs to wait before it can try to access an authenticated page. It can wait in several ways, such as:

  • It can start one long timeout, like in the POC. However, production applications should not take this approach.
  • Monolithic web applications can check access in short intervals.
  • SPAs and Monoliths can wait for a message from a web socket.
setTimeout(() => {
  location.href = "http://localhost:3000/dashboard"
 }, 30000)

API

The API needs to support two methods in this flow.

GET /qr-code

This endpoint returns an image content type, where the image is the QR code. The QR code encodes a newly generated session token. It also piggybacks a cookie with the same session token.

app.get("/qr-code", async (_req, res) => {
    const sessionId = await sessionManager.createSession()
    const qrRequestUrl = `${qrCodeBaseUrl}?size=150x150&data=${sessionId}`

    https.get(qrRequestUrl, (qrRes) => {
        const {statusCode, statusMessage} = qrRes
        if (statusCode !== 200) {
            console.error(statusMessage)
            qrRes.resume()
            return
        }

        res.setHeader("Content-Type", qrRes.headers["content-type"])
        res.setHeader("x-qr-session-id", sessionId)
        res.cookie("qr-session", sessionId, {
                maxAge: 5 * 60 * 1000,
                signed: false
            })

        qrRes.on("data", (chunk) => {
            res.write(chunk)
        })

        qrRes.on("end", () => {
            res.end()
        })
    })    
})

POST /login

This endpoint will accept a session token and user identification information such as username, authentication tokens, etc. If the session token and user identification are valid, the endpoint will update the session with the user identification information.

app.post("/login", (req, res) => {
    const sidParam = req.query.sessionId
    const sessionAttrs = sessionManager.getSession(sidParam)
    if (!sessionAttrs) {
        res.sendStatus(404)
        return
    }

    const userName = req.query.userName
    const updatedAttrs = sessionManager.patchSession(sidParam, {userName})
    res.send(updatedAttrs)
})

Mobile App

The user logs into the mobile app and scans the QR code. On the successful scan, it posts the session token from the QR code and user identification data such as username to the /login API endpoint.

super.onActivityResult(requestCode, resultCode, data)
val result = IntentIntegrator.parseActivityResult(resultCode, data)
if (result != null && this.userName != null) {
    val textView = findViewById<TextView>(R.id.textView)
    textView.text = result.contents
    val call = authService.login(result.contents, this.userName.toString())
    call.enqueue(object : Callback<String> {
        override fun onResponse(call: Call<String>, response: Response<String>) {
            Log.d("MainActivity", response.body() ?: "<empty response>")
        }

        override fun onFailure(call: Call<String>, t: Throwable) {
            Log.e("MainActivity", "Failed to login", t)
        }
    })
}

Spin it up

Web server

For this POC, we will use Python's built-in HTTP server.

$ python -m http.server 
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

API

$ npm start
 
> qr-api@0.0.1 start 
> node index.js 
 
Listening on port 3000

Open the browser

QR Code

Open the Mobile

For the POC, there is no actual authentication in the mobile app. Instead, it just accepts a username to log in. Then, on successfully scanning the QR code, it will post the username and decoded session token to the /login API endpoint. Mobile App

API Requests

The API logs the requests it gets. In this flow, it gets the following requests in order. The log also shows the response code for each request.

curl http://localhost:3000/qr-code -o qr.png
#GET /qr-code 200
curl -v http://localhost:3000/login\?sessionId\=0.4851294361\&userName\=sayantam -X POST
#POST /login?sessionId=0.4851294361&userName=sayantam 200
curl -v http://localhost:3000/dashboard --cookie "qr-session=0.4851294361"
#GET /dashboard 200 

The /dashboard request is the authenticated page.

Source Code

The source code for the POC is divided into two parts in a mono-repository.

  • qr-reader: Mobile App
  • qr-api: Web App & API
Enjoyed this post? Follow this blog to never miss out on future posts!

Related Posts

⏳

Effective Dependency Inversion
Dec 25 2022

Can you spot the issues in the following code snippet.

⏳

Flux-CD Pattern for AWS CDK8s Services
Jul 9 2023

AWS Cloud Development Kit for Kubernetes called generates Kubernetes manifest files for Kubernetes () deployments and services.

⏳

How Headless CMS work
Jun 25 2023

Headless CMSs came about because it is hard to build a single platform that content writers like using and software developers like…

⏳

How Time-based OTP (TOTP) works
Sep 10 2023

The adoption of two-factor authentication (2FA) has been steadily growing over the past three years.

⏳

JAM Stack
Nov 29 2020

In a previous post on how this blog works, I referred to Gatsby and GraphQL.

⏳

The First Post
Apr 20 2019

Just like fashion, technology tends to go in circles, and static web sites are rising in popularity for content.