QR Code Authentication

A QR code displayed on a desktop or kiosk device.

QR code authentication uses device credentials to enable cross-device authentication. This method leverages public key cryptography where private keys are securely stored on the user’s device.
This method uses the Device SDK to sign a challenge with the user’s device credentials.Device enrollment required: Users must have a device that they can use to complete the challenge. This can be done by adding device credentials via our mobile SDK.

Use cases

  • Log into a desktop or web application from an application that is typically used on mobile
  • Identify a user on a kiosk in a quick service restaurant (QSR) to load loyalty programs, rewards and offers
  • Log into an application running on a TV
  • Complete a payment via a terminal

Sequence diagram

The general flow for QR code authentication is as follows:
  1. Generate a challenge
  2. Display the QR code to the user
  3. Wait for the user to scan and respond on their mobile device
  4. Handle the authentication result (approved/rejected)

Configure QR code auth in Authsignal Portal

Enable the Device credential authentication method for your tenant.

Grab your Authsignal credentials

Head to Settings and grab your Tenant ID, API URL and API secret key. Add them as environment variables in your project:
AUTHSIGNAL_API_URL=your_region_api_url
AUTHSIGNAL_TENANT_ID=your_tenant_id
AUTHSIGNAL_SECRET_KEY=your_secret_key

Implementation

1. Frontend – Present a QR code

Use our Browser SDK to initiate an anonymous QR code challenge. This is called an anonymous challenge because it is not associated with a user. Start a QR code challenge Use the qrCode.challenge() method to initiate a challenge. The SDK will handle all the complexity of managing the challenge lifecycle, including automatic refreshing and state updates. By default the SDK will use WebSocket connections. If your environment does not support WebSockets, you can fall back to polling using REST API calls by setting polling to true.
const { data } = await authsignal.qrCode.challenge({
  action: "login-with-qr-code",
  onStateChange: (state, token) => {
    switch (state) {
      case "claimed":
        // User has scanned the QR code - blur/hide it
        console.log("QR code claimed by user");
        break;
      case "approved":
        // Challenge approved - validate the token on your backend
        if (token) {
          validateChallengeOnBackend(token);
        }
        break;
      case "rejected":
        // Challenge rejected - show error and offer retry
        console.log("Challenge rejected by user");
        break;
    }
  },
  onRefresh: ({ challengeId, expiresAt }) => {
    // Update your QR code display with the new challengeId
    updateQRCode(challengeId);
  },
  custom: {
    // Optional: Add context that will be shown on the user's device
    deviceInfo: "Terminal A",
  },
  refreshInterval: 540000, // 9 minutes (default)
  polling: false, // Use WebSocket for real-time updates (default)
});

// Display the QR code using the challengeId
if (data?.challengeId) {
  displayQRCode(data.challengeId);
}
State changes
1

Initial state

After calling qrCode.challenge(), the QR code is ready to be scanned. Display it to the user.
2

Claimed

When onStateChange is called with state: "claimed", the user has scanned the QR code. You should blur or hide the QR code at this point to prevent others from scanning it.
3

Approved

When onStateChange is called with state: "approved" and a token, the user has approved the challenge. Pass the token to your backend for validation.
4

Rejected

When onStateChange is called with state: "rejected", the user has declined the challenge. Show an error message and provide a way to retry.
Refresh a challenge (optional) If you need to manually refresh a challenge you can use the qrCode.refresh() method:
// Manually refresh with optional updated context
await authsignal.qrCode.refresh({
  custom: {
    deviceInfo: "Terminal B",
  },
});
This must be called after qrCode.challenge() method has been called. The original callbacks will be used with the new challenge.

2. Mobile App – Scan the QR code

Once the user scans the QR code, use the mobile sdk claimChallenge method to set the user attempting to complete the challenge. The claimChallenge method will return some context about the desktop or kiosk initiating the challenge such as ip address, location, user agent and custom data. This data can be shown to the user to help them decide if they want to approve or decline the challenge.
await authsignal.device.claimChallenge(
    challengeId: challengeId
)

3. Mobile App – Approve or decline the challenge

Present a dialog to allow the user to review the challenge context and approve or decline the challenge by calling the mobile sdk updateChallenge method.
await authsignal.device.updateChallenge(
    challengeId: challengeId,
    approved: true
)

4. Backend – Validate the challenge

Once the challenge is approved, the onStateChange callback will return an accessToken that should be passed to your backend to validate the challenge prior to completing the authentication flow.
const request = {
  token: "eyJhbGciOiJ...",
};

const response = await authsignal.validateChallenge(request);

if (response.state === "CHALLENGE_SUCCEEDED") {
  // The user completed the challenge successfully
  // Proceed with authenticated action or integrate with IdP to create authenticated session
} else {
  // The user did not complete the challenge successfully
}
That’s it! You’ve successfully implemented QR code authentication with Authsignal.

Next steps

  • Passkeys - Offer the most secure and user-friendly passwordless authentication
  • Push notification - Implement push notification authentication
  • Trusted device - Implement trusted device authentication